Composition¶
In the Composition mode several shifts act at once. It is declared by the list of shifts: every entry becomes one wrapper, and the order of the list is the order in which the wrappers are stacked.
Properties¶
| Aspect | Composition |
|---|---|
| Built by | One wrapper per shift, stacked on the task |
| Declared by | The order of the shifts list |
| Order | The first entry sits closest to the task |
| Shifts | Every shift source, in any combination |
| Seeding | Pipeline seed, per-shift seed and reset seed |
| Limit | One physical parameter and index is addressed by one shift only |
Supported shifts¶
Every mode name of every shift can be part of a composition.
| Shift | Mode names it accepts in this mode |
|---|---|
| Dynamic shift | scale, set, translate, gauss, uniform, loguniform, push; gauss on the target transition |
| Observation shift | gauss, uniform, relative, bias, shift, adversarial |
| Action shift | gauss, uniform, shift, adversarial, oppose, rotate, oppose_goal |
| Reward/cost shift | gauss, uniform, shift |
| Latency shift | fixed, buffer, substep, interp, delay |
| Semantic shift | hue, tint, swap, scale, set, translate, gauss, uniform, loguniform |
Declare the mode¶
import numpy as np
from robustrllib import make_robust, ShiftSpec
env = make_robust("Hopper-v5", shifts=[
ShiftSpec("dynamics", "scale", {"param": "gravity", "factor": 1.2}),
ShiftSpec("dynamics", "scale", {"param": "actuator_gear", "index": "all", "factor": 0.8}),
ShiftSpec("latency", "fixed", {"steps": 1}),
ShiftSpec("observation", "gauss", {"sigma": 0.05}),
], seed=0)
env.reset(seed=0)
print(env)
<ObservationShift<LatencyShift<DynamicsShift<DynamicsShift<RobustCoreWrapper<TimeLimit<OrderEnforcing<PassiveEnvChecker<HopperEnv<Hopper-v5>>>>>>>>>>
The same composition as a condition of an evaluation grid:
- name: gravity_gear_delay_noise
shifts:
- {target: dynamics, mode: scale, params: {param: gravity, factor: 1.2}}
- {target: dynamics, mode: scale, params: {param: actuator_gear, index: all, factor: 0.8}}
- {target: latency, mode: fixed, params: {steps: 1}}
- {target: observation, mode: gauss, params: {sigma: 0.05}}
A composition of a Semantic shift and a displacement crosses two shift factors in one condition.
crossed = [
ShiftSpec("appearance", "hue", {"geom": "torso_geom", "degrees": 120}),
ShiftSpec("dynamics", "translate", {"param": "body_pos_xyz", "index": "foot",
"offset": [0.06, 0.0, 0.0]}),
]
env = make_robust("Hopper-v5", shifts=crossed, seed=0)
Two Dynamic shifts on the same parameter and index do not multiply. Each sets the parameter
from its nominal value, and the later one wins; the pipeline emits a UserWarning.
env = make_robust("Hopper-v5", shifts=[
ShiftSpec("dynamics", "scale", {"param": "gravity", "factor": 1.2}),
ShiftSpec("dynamics", "scale", {"param": "gravity", "factor": 1.5}),
], seed=0)
env.reset(seed=0)
gravity_z = round(float(env.unwrapped.model.opt.gravity[2]), 3)
Gravity ends at -14.715, which is 1.5 times its nominal value and not 1.8 times.
Stacking order¶
A call to step enters at the outermost wrapper and travels inward; the observation travels
back outward.
| Direction | Processed first | Consequence |
|---|---|---|
| The action, inward | The last shift in the list | [latency, action] delays the noisy command; [action, latency] adds noise to the delayed command |
| The observation, outward | The first shift in the list | A later shift sees the output of the earlier ones |
reset |
The Dynamic shift recomputes the first observation | The output of the wrappers inside it is replaced |
gravity = ShiftSpec("dynamics", "scale", {"param": "gravity", "factor": 1.3})
noise = ShiftSpec("observation", "gauss", {"sigma": 0.05})
for label, shifts in [("dynamics, observation", [gravity, noise]),
("observation, dynamics", [noise, gravity])]:
env = make_robust("Hopper-v5", shifts=shifts, seed=0)
obs, _ = env.reset(seed=0)
noisy_reset = not np.allclose(obs, env.unwrapped._get_obs())
obs, *_ = env.step(np.zeros(3))
noisy_step = not np.allclose(obs, env.unwrapped._get_obs())
print(f"{label:24s}reset noisy: {noisy_reset} step noisy: {noisy_step}")
dynamics, observation reset noisy: True step noisy: True
observation, dynamics reset noisy: False step noisy: True
With the Observation shift listed first, the first observation of every episode reaches the policy without noise.
Seeding and reproducibility¶
Every shift wrapper owns a random generator.
| Seed | Set by | Takes effect |
|---|---|---|
| Pipeline seed | make_robust(..., seed=s) |
At construction, for shifts without a seed of their own |
| Per-shift seed | ShiftSpec(..., seed=s) |
At construction, for that shift |
| Reset seed | env.reset(seed=s) |
At that reset, for the task and every wrapper; overrides both |
def trace(pipeline_seed, reset_seed, spec_seed=None, steps=5):
env = make_robust("Hopper-v5", shifts=[
ShiftSpec("observation", "gauss", {"sigma": 0.05}, seed=spec_seed),
], seed=pipeline_seed)
obs, _ = env.reset(seed=reset_seed)
out = [obs]
for _ in range(steps):
obs, *_ = env.step(np.zeros(3))
out.append(obs)
env.close()
return np.array(out)
print("same seeds ", np.array_equal(trace(0, 0), trace(0, 0)))
print("pipeline seed differs ", np.array_equal(trace(0, 7), trace(5, 7)))
print("per-shift seed differs ", np.array_equal(trace(0, 7, spec_seed=1), trace(0, 7, spec_seed=2)))
print("reset seed differs ", np.array_equal(trace(0, 7), trace(0, 8)))
same seeds True
pipeline seed differs True
per-shift seed differs True
reset seed differs False
Once reset receives a seed, that seed alone determines the rollout. An evaluator that resets
an episode with the same seed under every condition obtains paired conditions.
The generator of a wrapper is seeded with the seed and a salt. The salt is a hash of the target,
the mode name and the param and index entries of the parameters, and it is stable across
processes, so different shifts draw independently from one seed.
Tip
The action space has a generator of its own. A rollout that uses
env.action_space.sample() calls env.action_space.seed(s) first.
Rules¶
- The list order is the stacking order; the first shift sits closest to the task.
- Dynamic shifts are listed before Observation shifts.
- One physical parameter and index is addressed by one shift only.
- Two shifts with the same target, mode name, parameter and index share a salt; listing the same noise shift twice adds the same noise twice.
- Every rollout that is compared with another is reset with a seed.
- A per-shift seed separates two shifts only as long as
resetis called without a seed.