Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Engine configuration utility #1828

Merged
merged 14 commits into from
Feb 10, 2023
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ Copy and pasting the git commit messages is __NOT__ enough.

## [Unreleased]
### Added
- Engine configuration utility that uses the following locations to allow configuration of the SMARTS engine. The engine consumes the configuration files from the following locations in the following priority: `./engine.ini`, `~/.smarts/engine.ini`, `$GLOBAL_USER/smarts/engine.ini`, and `${PYTHON_ENV}/lib/${PYTHON_VERSION}/site-packages/smarts/engine.ini`.
- Added map source uri as `map_source` inside of `hiway-v1` reset info to indicate what the current map is on reset.
### Changed
- Made changes in the docs to reflect `master` branch as the main development branch.
Expand Down
1 change: 1 addition & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ include smarts/core/glsl/*.vert smarts/core/glsl/*.frag
include smarts/core/models/*.glb smarts/core/models/*.urdf
include smarts/core/models/controller_parameters.yaml
include envision/web/dist/*
include smarts/*.ini
recursive-include smarts/benchmark *.yaml *.yml
recursive-include smarts/ros/src *.launch *.msg *.srv package.xml CMakeLists.txt *.py
recursive-include smarts/scenarios *.xml *.py
Expand Down
3 changes: 3 additions & 0 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
"sphinx.ext.coverage", # to generate documentation coverage reports
"sphinx.ext.extlinks", # shorten external links
"sphinx.ext.napoleon", # support Numpy and Google doc style
"sphinx.ext.todo", # support for todo items
"sphinx.ext.viewcode", # link to sourcecode from docs
"sphinx_rtd_theme", # Read The Docs theme
"sphinx_click", # extract documentation from a `click` application
Expand Down Expand Up @@ -90,6 +91,8 @@
"waymo_open_dataset",
]

todo_include_todos = True

# Add any paths that contain templates here, relative to this directory.
templates_path = ["_templates"]

Expand Down
2 changes: 1 addition & 1 deletion docs/ecosystem/sumo.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ SUMO

Learn SUMO through its user `documentation <https://sumo.dlr.de/docs/index.html>`_ .

SMARTS currently directly installs SUMO version >=1.12.0 via `pip`.
SMARTS currently directly installs SUMO version >=1.15.0 via `pip`.

.. code-block:: bash

Expand Down
4 changes: 3 additions & 1 deletion docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ If you use SMARTS in your research, please cite the `paper <https://arxiv.org/ab
sim/simulator.rst
sim/scenario_studio.rst
sim/bubbles.rst
sim/configuration.rst

.. toctree::
:hidden:
Expand Down Expand Up @@ -87,4 +88,5 @@ If you use SMARTS in your research, please cite the `paper <https://arxiv.org/ab
resources/containers.rst
resources/diagnostic.rst
resources/faq.rst
resources/contributing.rst
resources/contributing.rst
resources/todo.rst
6 changes: 5 additions & 1 deletion docs/resources/contributing.rst
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,10 @@ Execute the following to build the docs locally.
$ python3.8 -m http.server 8000 --bind 127.0.0.1 -d docs/_build/html
# Open http://127.0.0.1:8000 in your browser

If documentation is incomplete please mark the area with a ``.. todo::`` as described in `sphinx.ext.todo <https://www.sphinx-doc.org/en/master/usage/extensions/todo.html>`_.

Feel free to further contribute to the documentation and look at :ref:`todo` for sections that may yet need to be filled.

Communication
-------------

Expand Down Expand Up @@ -141,4 +145,4 @@ Things inevitably become slow, when this happens, Flame Graph is a great tool to
$ mkdir -p flamegraph_dir
$ curl https://raw.githubusercontent.com/brendangregg/FlameGraph/master/flamegraph.pl > ./utils/third_party/tools/flamegraph.pl
$ chmod 777 {$flamegraph_dir}/flamegraph.pl
$ make flamegraph scenario=./scenarios/sumo/loop script=./examples/control/chase_via_points.py
$ make flamegraph scenario=./scenarios/sumo/loop script=./examples/control/chase_via_points.py
8 changes: 8 additions & 0 deletions docs/resources/todo.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
.. _todo:

TODO List
=========

A list of current documentation TODO.

.. todolist_::
27 changes: 27 additions & 0 deletions docs/sim/configuration.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
.. _configuration:

Configuration
=============

You can change the behavior of the underlying SMARTS engine.

Configuration of the engine can come from several sources. These locations take precedence as noted:

1. Individual ``SMARTS_`` prefixed environment variables (e.g. ``SMARTS_SENSOR_WORKER_COUNT``)
2. Local directory engine configuration (./smarts_engine.ini )
3. Local user engine configuration, ``~/.smarts/engine.ini``, if local directory configuration is not found.
4. Global engine configuration, ``/etc/smarts/engine.ini``, if local configuration is not found.
5. Package default configuration, ``$PYTHON_PATH/smarts/engine.ini``, if global configuration is not found.

Note that configuration files resolve all settings at the first found configuration file (they do not layer.)

Options
-------

All settings demostrated as environment variables are formatted to ``UPPERCASE`` and prefixed with ``SMARTS_`` (e.g. ``[core] logging`` can be configured with ``SMARTS_CORE_LOGGING``)
Gamenot marked this conversation as resolved.
Show resolved Hide resolved

These settings are as follows:

.. todo::

List engine settings
1 change: 0 additions & 1 deletion smarts/benchmark/entrypoints/benchmark_runner_v0.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@
import ray

from smarts.benchmark.driving_smarts import load_config
from smarts.benchmark.driving_smarts.v0 import DEFAULT_CONFIG
from smarts.core.utils.logging import suppress_output
from smarts.env.gymnasium.wrappers.metrics import Metrics, Score
from smarts.zoo import registry as agent_registry
Expand Down
57 changes: 57 additions & 0 deletions smarts/core/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,13 @@

import random
import uuid
from functools import lru_cache, partial
from pathlib import Path

import numpy as np

from smarts.core.configuration import Config

_current_seed = None


Expand All @@ -48,3 +52,56 @@ def gen_id():
"""Generates a unique but deterministic id if `smarts.core.seed` has set the core seed."""
id_ = uuid.UUID(int=random.getrandbits(128))
return str(id_)[:8]


@lru_cache(maxsize=1)
def config(default: str = "./smarts_engine.ini") -> Config:
"""Get the SMARTS environment config for the smarts engine.

.. note::

This searches the following locations and loads the first one it finds:
./smarts_engine.ini
~/.smarts/engine.ini
/etc/smarts/engine.ini
$PYTHON_PATH/smarts/engine.ini

Args:
default (str, optional): The default configurable location. Defaults to "./smarts_engine.ini".

Returns:
Config: A configuration utility that allows resolving environment and `engine.ini` configuration.
"""
from smarts.core.utils.file import smarts_global_user_dir, smarts_local_user_dir

def get_file(config_file: Path):
try:
if not config_file.is_file():
return ""
except PermissionError:
return ""

return str(config_file)

conf = partial(Config, environment_prefix="SMARTS")

file = get_file(Path(default).absolute())
if file:
return conf(file)

try:
local_dir = smarts_local_user_dir()
except PermissionError:
file = ""
else:
file = get_file(Path(local_dir) / "engine.ini")
if file:
return conf(file)

global_dir = smarts_global_user_dir()
file = get_file(Path(global_dir) / "engine.ini")
if file:
return conf(file)

default_path = Path(__file__).parents[1].resolve() / "engine.ini"
return conf(get_file(default_path))
113 changes: 113 additions & 0 deletions smarts/core/configuration.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
# MIT License
#
# Copyright (C) 2023. Huawei Technologies Co., Ltd. All rights reserved.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
import configparser
import functools
import os
from pathlib import Path
from typing import Any, Callable, Optional, Union

_UNSET = object()


class Config:
"""A configuration utility that handles configuration from file and environment variable.

Args:
config_file (Union[str, Path]): The path to the configuration file.
environment_prefix (str, optional): The prefix given to the environment variables. Defaults to "".

Raises:
FileNotFoundError: If the configuration file cannot be found at the given file location.
"""

def __init__(
self, config_file: Union[str, Path], environment_prefix: str = ""
) -> None:
self._config = configparser.ConfigParser(
interpolation=configparser.ExtendedInterpolation()
)
self._environment_prefix = environment_prefix.upper()
self._environment_variable_format_string = self._environment_prefix + "_{}_{}"

if isinstance(config_file, str):
config_file = Path(config_file)
config_file = config_file.resolve()
if not config_file.is_file():
raise FileNotFoundError(f"Configuration file not found at {config_file}")

self._config.read(str(config_file.absolute()))

@property
def environment_prefix(self):
"""The prefix that environment variables configuration is provided with."""
return self._environment_prefix

@functools.lru_cache(maxsize=100)
def get_setting(
self,
section: str,
option: str,
default: Any = _UNSET,
cast: Callable[[str], Any] = str,
) -> Optional[Any]:
"""Finds the given configuration checking the following in order: environment variable,
configuration file, and default.

Args:
section (str): The grouping that the configuration option is under.
option (str): The specific configuration option.
default (Any, optional): The default if the requested configuration option is not found. Defaults to _UNSET.
cast (Callable, optional): A function that takes a string and returns the desired type. Defaults to str.


Returns:
Optional[str]: The value of the configuration.

Raises:
KeyError: If the configuration option is not found in the configuration file and no default is provided.
configparser.NoSectionError: If the section in the configuration file is not found and no default is provided.
"""
env_variable = self._environment_variable_format_string.format(
section.upper(), option.upper()
)
setting = os.getenv(env_variable)
if setting is not None:
return cast(setting)
try:
value = self._config[section][option]
except (configparser.NoSectionError, KeyError):
if default is _UNSET:
raise
return default
return cast(value)

def __call__(
self,
section: str,
option: str,
default: Any = _UNSET,
cast: Callable[[str], Any] = str,
) -> Optional[Any]:
return self.get_setting(section, option, default, cast)

def __repr__(self) -> str:
return f"Config(config_file={ {k: dict(v.items()) for k, v in self._config.items(raw=True)} }, environment_prefix={self._environment_prefix})"
27 changes: 20 additions & 7 deletions smarts/core/smarts.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
from smarts.core.plan import Plan
from smarts.core.utils.logging import timeit

from . import models
from . import config, models
from .actor import ActorRole, ActorState
from .agent_interface import AgentInterface
from .agent_manager import AgentManager
Expand Down Expand Up @@ -728,7 +728,9 @@ def _setup_bullet_client(self, client: bc.BulletClient):
client.configureDebugVisualizer(
pybullet.COV_ENABLE_GUI, 0 # pylint: disable=no-member
)

max_pybullet_freq = config()(
"physics", "max_pybullet_freq", default=MAX_PYBULLET_FREQ, cast=int
)
# PyBullet defaults the timestep to 240Hz. Several parameters are tuned with
# this value in mind. For example the number of solver iterations and the error
# reduction parameters (erp) for contact, friction and non-contact joints.
Expand All @@ -741,11 +743,11 @@ def _setup_bullet_client(self, client: bc.BulletClient):
self._pybullet_period = (
self._fixed_timestep_sec
if self._fixed_timestep_sec
else 1 / MAX_PYBULLET_FREQ
else 1 / max_pybullet_freq
)
client.setPhysicsEngineParameter(
fixedTimeStep=self._pybullet_period,
numSubSteps=int(self._pybullet_period * MAX_PYBULLET_FREQ),
numSubSteps=int(self._pybullet_period * max_pybullet_freq),
numSolverIterations=10,
solverResidualThreshold=0.001,
# warmStartingFactor=0.99
Expand Down Expand Up @@ -857,10 +859,10 @@ def __del__(self):
" go away.",
e,
)
except (AttributeError, KeyboardInterrupt):
except (TypeError, KeyboardInterrupt):
return
raise exception

def _teardown_vehicles(self, vehicle_ids):
self._vehicle_index.teardown_vehicles_by_vehicle_ids(vehicle_ids)
self._clear_collisions(vehicle_ids)
Expand Down Expand Up @@ -1281,7 +1283,18 @@ def fixed_timestep_sec(self) -> float:
def fixed_timestep_sec(self, fixed_timestep_sec: float):
if not fixed_timestep_sec:
# This is the fastest we could possibly run given constraints from pybullet
self._rounder = rounder_for_dt(round(1 / MAX_PYBULLET_FREQ, 6))
self._rounder = rounder_for_dt(
round(
1
/ config()(
"physics",
"max_pybullet_freq",
default=MAX_PYBULLET_FREQ,
cast=int,
),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think pulling this out as a separate variable would make the expression more readable.

6,
)
)
else:
self._rounder = rounder_for_dt(fixed_timestep_sec)
self._fixed_timestep_sec = fixed_timestep_sec
Expand Down
Loading