Skip to content

Commit

Permalink
Merge pull request #3 from secorolab/cli
Browse files Browse the repository at this point in the history
Add command line interface
  • Loading branch information
Samuel-Wiest authored Jun 21, 2024
2 parents 82883d0 + 0fca94a commit 875fdd6
Show file tree
Hide file tree
Showing 12 changed files with 115 additions and 82 deletions.
70 changes: 35 additions & 35 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,52 +1,52 @@
# scenery_builder

## Installation

## Task generator

FloorPlan DSL inset and gazebo world generator
```shell
pip install -e .
```

Requirements
============
* Python 3: [https://www.python.org/]
* RDFLib: [https://github.com/RDFLib/rdflib]
* PyLD: [https://github.com/digitalbazaar/pyld]
* StringTemplate Standalone Tool (v4): [https://github.com/jsnyders/STSTv4]
## Usage

# Usage
This module adds `floorplan` as a command line interface. You can use the `generate` command as shown below:

```
python3 <input_folder_with_floorplan_models>
```shell
floorplan generate <path to config file> <path to input folder>
```

# Configuration
* Width of inset: float, distance between the sides of the inset and original shapes.
* output for waypoints: string, full path.
* output for models: string, full path.
* output for world files: string, full path.
* output for launch: string, full path. This launch files assumes that the world model is located in the pkg.
* pkg path: string, name of the ROS package.
Where the input folder must contain:
- the composable models generated from the [FloorPlan DSL](https://github.com/secorolab/FloorPlan-DSL)
- `coordinate.json`
- `floorplan.json`
- `shape.json`
- `skeleton.json`
- `spatial_relations.json`
- the door object models
- `object-door.json`
- `object-door-states.json`
- any object instance models, e.g. `object-door-instance-X.json` where `X` is a unique numeric ID.

## Object modelling and placing
## Task generator

This tool is a companion tool for the FloorPlan DSL, which enables the modelling of objects and their placement in the indoor environments from the FloorPlan DSL.
It uses the FloorPlan insets to generate a task specification.
The inset width -- a float value representing the distance between the sides of the inset and original shapes -- can be configured in the [config](config/config.toml)

![](images/gazebo-screenshot.png)
## Object placing

## Features:
* **Model objects with movement constraits**: the tool enables the specification of objects with revolute, prismatic, or fixed joints.
* **Place objects in indoor environments**: by using the composable modelling approach the FloorPlan models can become scenes filled with objects with ease.
* **Model object states**: the tool allows for finate state machines for objects with motion constraints to be modelled, as well as selecting the intial state of each object in the scene.
* **Gazebo world generation and plugin**: the tool generates SDF format world files for gazebo, while a companion plugin sets up the scene as determined by the initial state. The plugin is available [here](https://github.com/hbrs-sesame/floorplan-gazebo-initial-state-plugin)
This tool places objects in indoor environments.
By using the composable modelling approach, a scenery can compose the static FloorPlan models with objects such as doors.

## Getting Startedith Python 3.8.10,
![](docs/images/gazebo-screenshot.png)

### Models that can be composed into a scenery

Install the [requirements](requirements.txt), then you may run the example:
* **Model objects with movement constraits**: composition of objects with revolute, prismatic, or fixed joints into a scenery.
* **Model object states**: composition of objects with motion constraints defined as finite state machines, and their intial state in the scene.

```sh
python3 main.py <input folder>
```
Tested on Python 3.8.10
## Gazebo world generation

The tool generates SDF format world files for Gazebo.
The [initial state plugin](https://github.com/secorolab/floorplan-gazebo-plugins) sets up the scene as determined by the initial state for each object included in the world file.

## Tutorials

Expand All @@ -57,6 +57,6 @@ Tutorials on how to model objects with movement constraints, and how to place th
This work is part of a project that has received funding from the European Union's Horizon 2020 research and innovation programme SESAME under grant agreement No 101017258.

<p align="center">
<img src="images/EU.jpg" alt="drawing" height="100"/>
<img src="images/SESAME.jpg" alt="drawing" height="100"/>
<img src="docs/images/EU.jpg" alt="drawing" height="100"/>
<img src="docs/images/SESAME.jpg" alt="drawing" height="100"/>
</p>
16 changes: 4 additions & 12 deletions config/config.toml
Original file line number Diff line number Diff line change
@@ -1,14 +1,6 @@
[output]
base_path = "../../output/consolidated"

tasks = "tasks"
gazebo_models = "models"
gazebo_worlds = "worlds"
ros_launch = "launch"
ros_pkg = "floorplan_models"

[transformations.tasks]
# Parametrize the transformations
[tasks]
inset_width = 0.7

[templates]
path = "../../templates"
[roslaunch]
ros_pkg = "floorplan_models"
6 changes: 6 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,11 @@ maintainers = [
[project.optional-dependencies]
dev = ["black>=24.4.2"]

[project.scripts]
floorplan = "fpm.cli:floorplan"

[tool.setuptools.package-data]
"*" = ["*.jinja"]

[tool.black]
required-version = "24.4.2"
80 changes: 54 additions & 26 deletions src/fpm/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,14 @@ def floorplan():
pass


def door_object_models(config, g):
template_path = config["templates"]["path"]
def door_object_models(g, base_path, **kwargs):
template_path = kwargs.get("template_path")

object_models = get_all_object_models(g)

for model in object_models:
model_name = model["name"][5:]
output_path = get_output_path(config, "gazebo_models", model_name)
output_path = get_output_path(base_path, "gazebo/models", model_name)
generate_sdf_file(
model,
output_path,
Expand All @@ -39,13 +39,10 @@ def door_object_models(config, g):
)


def gazebo_world(config, g, model_name):
template_path = config["templates"]["path"]

instances = get_all_object_instances(g)
model = {"instances": instances, "name": model_name}

output_path = get_output_path(config, "gazebo_models", model_name)
def gazebo_floorplan_model(model_name, base_path, **kwargs):
template_path = kwargs.get("template_path")
model = {"name": model_name}
output_path = get_output_path(base_path, "gazebo/models", model_name)
generate_sdf_file(
model,
output_path,
Expand All @@ -61,7 +58,13 @@ def gazebo_world(config, g, model_name):
template_path=template_path,
)

output_path = get_output_path(config, "gazebo_worlds")

def gazebo_world_model(g, model_name, base_path, **kwargs):
template_path = kwargs.get("template_path")
instances = get_all_object_instances(g)
model = {"instances": instances, "name": model_name}

output_path = get_output_path(base_path, "gazebo/worlds")
generate_sdf_file(
model,
output_path,
Expand All @@ -70,7 +73,10 @@ def gazebo_world(config, g, model_name):
template_path=template_path,
)

output_path = get_output_path(config, "ros_launch")

def gazebo_world_launch(model_name, base_path, **kwargs):
template_path = kwargs.get("template_path")
output_path = get_output_path(base_path, "ros/launch")
generate_launch_file(
model_name,
output_path,
Expand All @@ -79,38 +85,60 @@ def gazebo_world(config, g, model_name):
)


def tasks(config, g, model_name):
output_path = get_output_path(config, "tasks", model_name)
inset_width = config["transformations"]["tasks"]["inset_width"]
def gazebo_world(g, model_name, base_path, **kwargs):

gazebo_floorplan_model(model_name, base_path, **kwargs)
gazebo_world_model(g, model_name, base_path, **kwargs)
gazebo_world_launch(model_name, base_path, **kwargs)


def tasks(g, base_path, config, **kwargs):
output_path = get_output_path(base_path, "tasks")
inset_width = config["tasks"]["inset_width"]

tasks = get_all_disinfection_tasks(g, inset_width)
for task in tasks:
generate_task_specification(task, output_path)


def get_output_path(config, model_type, model_name=None):
output_config = config.get("output")
subfolder = output_config.get(model_type)
def get_output_path(base_path, subfolder, model_name=None):
if model_name is None:
return os.path.join(output_config["base_path"], subfolder)
return os.path.join(base_path, subfolder)
else:
return os.path.join(output_config["base_path"], subfolder, model_name)
return os.path.join(base_path, subfolder, model_name)


@floorplan.command()
@click.argument("configfile", type=click.Path(exists=True, resolve_path=True))
@click.argument("inputs", type=click.Path(exists=True, resolve_path=True))
def generate(configfile, inputs):
click.echo(click.format_filename(configfile))
@click.option(
"--inputs",
"-i",
type=click.Path(exists=True, resolve_path=True),
required=True,
multiple=True,
)
@click.option(
"--output-path",
type=click.Path(exists=True, resolve_path=True),
default=os.path.join("."),
)
@click.option(
"--templates",
type=click.Path(exists=True, resolve_path=True),
default=os.path.join("."),
)
def generate(configfile, inputs, output_path, **kwargs):
config = load_config_file(configfile)

g = build_graph_from_directory(inputs)
model_name = get_floorplan_model_name(g)

tasks(config, g, model_name)
base_path = os.path.join(output_path, model_name)

tasks(g, base_path, config)

door_object_models(config, g)
gazebo_world(config, g, model_name)
door_object_models(g, base_path, **kwargs)
gazebo_world(g, model_name, base_path, **kwargs)


if __name__ == "__main__":
Expand Down
2 changes: 1 addition & 1 deletion src/fpm/generators/gazebo.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@


def generate_sdf_file(
model, output_folder, file_name, template_name, template_path="templates"
model, output_folder, file_name, template_name, template_path=None
):

template = load_template(template_name, template_path)
Expand Down
12 changes: 8 additions & 4 deletions src/fpm/graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,16 @@
from fpm.utils import build_transformation_matrix


def build_graph_from_directory(input_folder):
def build_graph_from_directory(inputs: tuple):
# Build the graph by reading all composable models in the input folder
g = rdflib.Graph()
input_models = glob.glob(os.path.join(input_folder, "*.json"))
for file_path in input_models:
g.parse(file_path, format="json-ld")
for input_folder in inputs:
input_models = glob.glob(os.path.join(input_folder, "*.json"))
print("Found {} models in {}".format(len(input_models), input_folder))
print("Adding to the graph...")
for file_path in input_models:
print("\t{}".format(file_path))
g.parse(file_path, format="json-ld")

return g

Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
11 changes: 7 additions & 4 deletions src/fpm/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

import numpy as np

from jinja2 import Environment, FileSystemLoader
from jinja2 import Environment, FileSystemLoader, PackageLoader


def load_config_file(file_path):
Expand All @@ -14,9 +14,12 @@ def load_config_file(file_path):
return data


def load_template(template_name, template_folder):
file_loader = FileSystemLoader(template_folder)
env = Environment(loader=file_loader)
def load_template(template_name, template_folder=None):
if template_folder is None:
loader = PackageLoader("fpm")
else:
loader = FileSystemLoader(template_folder)
env = Environment(loader=loader)
return env.get_template(template_name)


Expand Down

0 comments on commit 875fdd6

Please sign in to comment.