├── .gitignore
├── CONTRIBUTING.md
├── Dockerfile
├── LICENSE
├── Machine-Teaching-Examples
├── moab_action_transform.ink
├── moab_pomdp.ink
├── moab_state_transform.ink
├── moab_stochastic.ink
├── moab_with_noise.ink
└── model_import
│ ├── LAW.png
│ ├── README.md
│ ├── environment.yml
│ ├── imported-concept-graph.png
│ ├── moab-imported-concept.ink
│ ├── state_transform_deep.onnx
│ ├── state_transform_deep.zip
│ ├── state_transform_keras_model.ipynb
│ └── training-graph.png
├── README.md
├── SECURITY.md
├── assess_config.json
├── moab.sh
├── moab_experiment.ink
├── moab_interface.json
├── moab_model.py
├── moab_sim.py
├── moab_tutorial_1.ink
├── moab_tutorial_2.ink
├── moab_tutorial_3.ink
├── requirements.txt
├── test_moab_model.py
├── test_moab_perf.py
├── test_moab_sim.py
└── tests
├── __init__.py
├── conftest.py
└── test_model_import.py
/.gitignore:
--------------------------------------------------------------------------------
1 | *.egg-info
2 | __pycache__
3 | build
4 | test-results
5 |
6 | # Internal inkling that is not to be pushed to public github
7 | base.ink
8 | moab_tutorial_1_tpl.ink
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | This project welcomes contributions and suggestions. Most contributions require you to
4 | agree to a Contributor License Agreement (CLA) declaring that you have the right to,
5 | and actually do, grant us the rights to use your contribution. For details, visit
6 | https://cla.microsoft.com.
7 |
8 | When you submit a pull request, a CLA-bot will automatically determine whether you need
9 | to provide a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the
10 | instructions provided by the bot. You will only need to do this once across all repositories using our CLA.
11 |
12 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).
13 | For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/)
14 | or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.
15 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | # this is one of the cached base images available for ACI
2 | FROM python:3.7.4
3 |
4 | # Install libraries and dependencies
5 | RUN apt-get update && \
6 | apt-get install -y --no-install-recommends \
7 | && rm -rf /var/lib/apt/lists/*
8 |
9 | # Install dependencies
10 | RUN pip3 install -U setuptools \
11 | && pip3 install git+https://github.com/microsoft/bonsai-common \
12 | && pip3 uninstall -y setuptools
13 |
14 | # Set up the simulator
15 | WORKDIR /sim
16 |
17 | # Copy simulator files to /sim
18 | COPY ./ /sim
19 |
20 | # Install simulator dependencies
21 | RUN pip3 install -r requirements.txt
22 |
23 | # This will be the command to run the simulator
24 | CMD ["python", "moab_sim.py"]
25 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 - Present, Microsoft Corporation
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/Machine-Teaching-Examples/moab_action_transform.ink:
--------------------------------------------------------------------------------
1 | ###
2 |
3 | # MSFT Bonsai
4 | # Copyright 2021 Microsoft
5 | # This code is licensed under MIT license (see LICENSE for details)
6 |
7 | # Moab Sample illustrating how to use an action transform to train a concept
8 | # with an action space that differs from the actions expected by the simulator.
9 |
10 | ###
11 |
12 | inkling "2.0"
13 |
14 | using Math
15 | using Goal
16 |
17 | # Distances measured in meters
18 | const RadiusOfPlate = 0.1125 # m
19 |
20 | # Velocities measured in meters per sec.
21 | const MaxVelocity = 6.0
22 | const MaxInitialVelocity = 1.0
23 |
24 | # Threshold for ball placement
25 | const CloseEnough = 0.02
26 |
27 | # Default time delta between simulation steps (s)
28 | const DefaultTimeDelta = 0.045
29 |
30 | # Maximum distance per step in meters
31 | const MaxDistancePerStep = DefaultTimeDelta * MaxVelocity
32 |
33 | # Maximum plate angle in degrees
34 | const MaxPlateAngle = 22
35 |
36 | # State received from the simulator after each iteration
37 | type ObservableState {
38 | # Ball X,Y position
39 | ball_x: number<-MaxDistancePerStep - RadiusOfPlate .. RadiusOfPlate + MaxDistancePerStep>,
40 | ball_y: number<-MaxDistancePerStep - RadiusOfPlate .. RadiusOfPlate + MaxDistancePerStep>,
41 |
42 | # Ball X,Y velocity
43 | ball_vel_x: number<-MaxVelocity .. MaxVelocity>,
44 | ball_vel_y: number<-MaxVelocity .. MaxVelocity>,
45 | }
46 |
47 | # Action learned by the concept, and passed to the action transform.
48 | # pitch and roll actions in degrees.
49 | type BrainAction {
50 | # Range is in degrees
51 | # the full plate rotation range supported by the hardware.
52 | input_pitch: number<-MaxPlateAngle .. MaxPlateAngle>, # rotate about x-axis
53 | input_roll: number<-MaxPlateAngle .. MaxPlateAngle>, # rotate about y-axis
54 | }
55 |
56 | # Action expected by the sim, with pitch and roll normalized between -1 and 1
57 | type SimAction {
58 | # Range -1 to 1 is a scaled value that represents
59 | # the full plate rotation range supported by the hardware.
60 | input_pitch: number<-1 .. 1>, # rotate about x-axis
61 | input_roll: number<-1 .. 1>, # rotate about y-axis
62 | }
63 |
64 | # Per-episode configuration that can be sent to the simulator.
65 | type SimConfig {
66 | # Model initial ball conditions
67 | initial_x: number<-RadiusOfPlate .. RadiusOfPlate>, # in (m)
68 | initial_y: number<-RadiusOfPlate .. RadiusOfPlate>,
69 |
70 | # Model initial ball velocity conditions
71 | initial_vel_x: number<-MaxInitialVelocity .. MaxInitialVelocity>, # in (m/s)
72 | initial_vel_y: number<-MaxInitialVelocity .. MaxInitialVelocity>,
73 |
74 | # Range -1 to 1 is a scaled value that represents
75 | # the full plate rotation range supported by the hardware.
76 | initial_pitch: number<-1 .. 1>,
77 | initial_roll: number<-1 .. 1>,
78 | }
79 |
80 | # Action transform function definition from degrees to normalized
81 | function TransformAction(a: BrainAction): SimAction {
82 | return {
83 | input_pitch: a.input_pitch / MaxPlateAngle,
84 | input_roll: a.input_roll / MaxPlateAngle,
85 | }
86 | }
87 |
88 | # Define a concept graph with a single concept
89 | graph (input: ObservableState) {
90 | concept MoveToCenter(input): BrainAction {
91 | curriculum {
92 | # The source of training for this concept is a simulator that
93 | # - can be configured for each episode using fields defined in SimConfig,
94 | # - accepts per-iteration actions defined in SimAction, and
95 | # - outputs states with the fields defined in SimState.
96 | source simulator MoabSim(Action: SimAction, Config: SimConfig): ObservableState {
97 | # Automatically launch the simulator with this
98 | # registered package name.
99 | package "Moab"
100 | }
101 |
102 | training {
103 | # Limit episodes to 250 iterations instead of the default 1000.
104 | EpisodeIterationLimit: 250
105 | }
106 |
107 | # Specify the action transformation function used when training or assessing using the simulator.
108 | # This transform is _not_ included when the brain is exported — it is only used in training and assessment.
109 | action TransformAction
110 |
111 | # The objective of training is expressed as a goal with two
112 | # subgoals: don't let the ball fall off the plate, and drive
113 | # the ball to the center of the plate.
114 | goal (State: ObservableState) {
115 | avoid `Fall Off Plate`: Math.Hypot(State.ball_x, State.ball_y) in Goal.RangeAbove(RadiusOfPlate * 0.8)
116 | drive `Center Of Plate`: [State.ball_x, State.ball_y] in Goal.Sphere([0, 0], CloseEnough)
117 | }
118 |
119 | lesson `Randomize Start` {
120 | # Specify the configuration parameters that should be varied
121 | # from one episode to the next during this lesson.
122 | scenario {
123 | initial_x: number<-RadiusOfPlate * 0.5 .. RadiusOfPlate * 0.5>,
124 | initial_y: number<-RadiusOfPlate * 0.5 .. RadiusOfPlate * 0.5>,
125 |
126 | initial_vel_x: number<-MaxInitialVelocity * 0.02 .. MaxInitialVelocity * 0.02>,
127 | initial_vel_y: number<-MaxInitialVelocity * 0.02 .. MaxInitialVelocity * 0.02>,
128 |
129 | initial_pitch: number<-0.2 .. 0.2>,
130 | initial_roll: number<-0.2 .. 0.2>,
131 | }
132 | }
133 | }
134 | }
135 | }
136 |
137 | # Special string to hook up the simulator visualizer
138 | # in the web interface.
139 | const SimulatorVisualizer = "/moabviz/"
140 |
--------------------------------------------------------------------------------
/Machine-Teaching-Examples/moab_pomdp.ink:
--------------------------------------------------------------------------------
1 | ###
2 |
3 | # MSFT Bonsai
4 | # Copyright 2021 Microsoft
5 | # This code is licensed under MIT license (see LICENSE for details)
6 |
7 | # Sample illustrating how brain memory helps solve partially observable
8 | # problems, where the available sensor information does not provide enough
9 | # information to control the system. The system learns to control the actions
10 | # based on the history of the system dynamics.
11 |
12 | # Note: This sample includes only the ball position in the observable state,
13 | # removing velocity. This is for illustration, and not recommended in real
14 | # problems: if there is information that you know is helpful for solving the
15 | # problem, like ball velocity for balancing a ball on a plate, include them!
16 | # Memory helps capture other history-dependent features that are difficulty
17 | # to explicitly define.
18 |
19 | ###
20 |
21 | inkling "2.0"
22 |
23 | using Math
24 | using Goal
25 |
26 | # Distances measured in meters
27 | const RadiusOfPlate = 0.1125 # m
28 |
29 | # Velocities measured in meters per sec.
30 | const MaxVelocity = 6.0
31 | const MaxInitialVelocity = 1.0
32 |
33 | # Threshold for ball placement
34 | const CloseEnough = 0.02
35 |
36 | # Default time delta between simulation steps (s)
37 | const DefaultTimeDelta = 0.045
38 |
39 | # Maximum distance per step in meters
40 | const MaxDistancePerStep = DefaultTimeDelta * MaxVelocity
41 |
42 | # State received from the simulator after each iteration
43 | type SimState {
44 | # Ball X,Y position
45 | ball_x: number<-MaxDistancePerStep - RadiusOfPlate .. RadiusOfPlate + MaxDistancePerStep>,
46 | ball_y: number<-MaxDistancePerStep - RadiusOfPlate .. RadiusOfPlate + MaxDistancePerStep>,
47 |
48 | # Ball X,Y velocity
49 | ball_vel_x: number<-MaxVelocity .. MaxVelocity>,
50 | ball_vel_y: number<-MaxVelocity .. MaxVelocity>,
51 | }
52 |
53 | # Deliberately remove ball velocity from the state passed to the brain, and
54 | # give it only the ball position. This turns this problem into a Partially
55 | # Observable Markov Decision Process (POMDP)
56 | type ObservableState {
57 | # Ball X,Y position
58 | ball_x: number<-MaxDistancePerStep - RadiusOfPlate .. RadiusOfPlate + MaxDistancePerStep>,
59 | ball_y: number<-MaxDistancePerStep - RadiusOfPlate .. RadiusOfPlate + MaxDistancePerStep>,
60 | }
61 |
62 | # Action provided as output by policy and sent as
63 | # input to the simulator
64 | type SimAction {
65 | # Range -1 to 1 is a scaled value that represents
66 | # the full plate rotation range supported by the hardware.
67 | input_pitch: number<-1 .. 1>, # rotate about x-axis
68 | input_roll: number<-1 .. 1>, # rotate about y-axis
69 | }
70 |
71 | # Per-episode configuration that can be sent to the simulator.
72 | # All iterations within an episode will use the same configuration.
73 | type SimConfig {
74 | # Model initial ball conditions
75 | initial_x: number<-RadiusOfPlate .. RadiusOfPlate>, # in (m)
76 | initial_y: number<-RadiusOfPlate .. RadiusOfPlate>,
77 |
78 | # Model initial ball velocity conditions
79 | initial_vel_x: number<-MaxInitialVelocity .. MaxInitialVelocity>, # in (m/s)
80 | initial_vel_y: number<-MaxInitialVelocity .. MaxInitialVelocity>,
81 |
82 | # Range -1 to 1 is a scaled value that represents
83 | # the full plate rotation range supported by the hardware.
84 | initial_pitch: number<-1 .. 1>,
85 | initial_roll: number<-1 .. 1>,
86 | }
87 |
88 | # Define a concept graph with a single concept
89 | graph (input: ObservableState) {
90 | concept MoveToCenter(input): SimAction {
91 | curriculum {
92 | # The source of training for this concept is a simulator that
93 | # - can be configured for each episode using fields defined in SimConfig,
94 | # - accepts per-iteration actions defined in SimAction, and
95 | # - outputs states with the fields defined in SimState.
96 | source simulator MoabSim(Action: SimAction, Config: SimConfig): SimState {
97 | # Automatically launch the simulator with this
98 | # registered package name.
99 | package "Moab"
100 | }
101 |
102 | training {
103 | # Limit episodes to 250 iterations instead of the default 1000.
104 | EpisodeIterationLimit: 250
105 | }
106 |
107 | algorithm {
108 | # Use supported values:
109 | # default - AI engine will choose automatically
110 | # none - no memory, learned actions depend on current state
111 | # state - memory of past states
112 | # state and action - memory of past states and actions.
113 | MemoryMode: "state and action"
114 | }
115 |
116 | # The objective of training is expressed as a goal with two
117 | # subgoals: don't let the ball fall off the plate, and drive
118 | # the ball to the center of the plate.
119 | goal (State: SimState) {
120 | avoid `Fall Off Plate`:
121 | Math.Hypot(State.ball_x, State.ball_y) in Goal.RangeAbove(RadiusOfPlate * 0.8)
122 | drive `Center Of Plate`:
123 | [State.ball_x, State.ball_y] in Goal.Sphere([0, 0], CloseEnough)
124 | }
125 |
126 | lesson `Randomize Start` {
127 | # Specify the configuration parameters that should be varied
128 | # from one episode to the next during this lesson.
129 | scenario {
130 | initial_x: number<-RadiusOfPlate * 0.6 .. RadiusOfPlate * 0.6>,
131 | initial_y: number<-RadiusOfPlate * 0.6 .. RadiusOfPlate * 0.6>,
132 |
133 | initial_vel_x: number<-MaxInitialVelocity * 0.2 .. MaxInitialVelocity * 0.2>,
134 | initial_vel_y: number<-MaxInitialVelocity * 0.2 .. MaxInitialVelocity * 0.2>,
135 |
136 | initial_pitch: number<-0.1 .. 0.1>,
137 | initial_roll: number<-0.1 .. 0.1>,
138 | }
139 | }
140 | }
141 | }
142 | }
143 |
144 | # Special string to hook up the simulator visualizer
145 | # in the web interface.
146 | const SimulatorVisualizer = "/moabviz/"
--------------------------------------------------------------------------------
/Machine-Teaching-Examples/moab_state_transform.ink:
--------------------------------------------------------------------------------
1 | ###
2 |
3 | # MSFT Bonsai
4 | # Copyright 2020 Microsoft
5 | # This code is licensed under MIT license (see LICENSE for details)
6 |
7 | # Moab Sample illustrating how to use a state transform to train a brain
8 | # with an input space that differs from the states output by the simulator.
9 |
10 | # When using an exported brain with this sample, ensure to use the
11 | # ObservableState type definition from the environment
12 |
13 | ###
14 |
15 | inkling "2.0"
16 |
17 | using Math
18 | using Goal
19 |
20 | # Distances measured in meters
21 | const RadiusOfPlate = 0.1125 # m
22 |
23 | # Velocities measured in meters per sec.
24 | const MaxVelocity = 6.0
25 | const MaxInitialVelocity = 1.0
26 |
27 | # Threshold for ball placement
28 | const CloseEnough = 0.02
29 |
30 | # Default time delta between simulation steps (s)
31 | const DefaultTimeDelta = 0.045
32 |
33 | # Maximum distance per step in meters
34 | const MaxDistancePerStep = DefaultTimeDelta * MaxVelocity
35 |
36 | # What velocity do we want the ball to have when it reaches the target?
37 | # (This could be configurable in the sim to make it non-constant. Kept here for simplicity.)
38 | const TargetVelocityX = 0
39 | const TargetVelocityY = 0
40 |
41 | # State received from the simulator after each iteration. It includes absolute positions and velocities.
42 | type SimState {
43 | # Ball X,Y position
44 | ball_x: number<-MaxDistancePerStep - RadiusOfPlate .. RadiusOfPlate + MaxDistancePerStep>,
45 | ball_y: number<-MaxDistancePerStep - RadiusOfPlate .. RadiusOfPlate + MaxDistancePerStep>,
46 |
47 | # Ball X,Y velocity
48 | ball_vel_x: number<-MaxVelocity .. MaxVelocity>,
49 | ball_vel_y: number<-MaxVelocity .. MaxVelocity>,
50 |
51 | # Target stationary X,Y position
52 | target_x: number<-MaxDistancePerStep - RadiusOfPlate .. RadiusOfPlate + MaxDistancePerStep>,
53 | target_y: number<-MaxDistancePerStep - RadiusOfPlate .. RadiusOfPlate + MaxDistancePerStep>,
54 | }
55 |
56 | # Brain input state, consisting of error from target value for position and velocity.
57 | type ObservableState {
58 | # Ball X,Y position
59 | ball_x_error: number<-MaxDistancePerStep - RadiusOfPlate .. RadiusOfPlate + MaxDistancePerStep>,
60 | ball_y_error: number<-MaxDistancePerStep - RadiusOfPlate .. RadiusOfPlate + MaxDistancePerStep>,
61 |
62 | # Ball X,Y velocity
63 | ball_vel_x_error: number<-MaxVelocity .. MaxVelocity>,
64 | ball_vel_y_error: number<-MaxVelocity .. MaxVelocity>,
65 | }
66 |
67 | # Action provided as output by policy and sent as
68 | # input to the simulator
69 | type SimAction {
70 | # Range -1 to 1 is a scaled value that represents
71 | # the full plate rotation range supported by the hardware.
72 | input_pitch: number<-1 .. 1>, # rotate about x-axis
73 | input_roll: number<-1 .. 1>, # rotate about y-axis
74 | }
75 |
76 | # Per-episode configuration that can be sent to the simulator.
77 | type SimConfig {
78 | # Model initial ball conditions
79 | initial_x: number<-RadiusOfPlate .. RadiusOfPlate>, # in (m)
80 | initial_y: number<-RadiusOfPlate .. RadiusOfPlate>,
81 |
82 | # Model initial ball velocity conditions
83 | initial_vel_x: number<-MaxInitialVelocity .. MaxInitialVelocity>, # in (m/s)
84 | initial_vel_y: number<-MaxInitialVelocity .. MaxInitialVelocity>,
85 |
86 | # Range -1 to 1 is a scaled value that represents
87 | # the full plate rotation range supported by the hardware.
88 | initial_pitch: number<-1 .. 1>,
89 | initial_roll: number<-1 .. 1>,
90 |
91 | # Target X,Y position
92 | target_x: number<-MaxDistancePerStep - RadiusOfPlate .. RadiusOfPlate + MaxDistancePerStep>,
93 | target_y: number<-MaxDistancePerStep - RadiusOfPlate .. RadiusOfPlate + MaxDistancePerStep>,
94 | }
95 |
96 | # State transform function definition from absolute to error
97 | # like commonly found in control theory
98 | function TransformState (s: SimState): ObservableState {
99 | return {
100 | ball_x_error: s.target_x - s.ball_x,
101 | ball_y_error: s.target_y - s.ball_y,
102 | ball_vel_x_error: TargetVelocityX - s.ball_vel_x,
103 | ball_vel_y_error: TargetVelocityY - s.ball_vel_y,
104 | }
105 | }
106 |
107 | # Define a concept graph with a single concept
108 | graph (input: ObservableState) {
109 | concept MoveToCenter(input): SimAction {
110 | curriculum {
111 | # The source of training for this concept is a simulator that
112 | # - can be configured for each episode using fields defined in SimConfig,
113 | # - accepts per-iteration actions defined in SimAction, and
114 | # - outputs states with the fields defined in SimState.
115 | source simulator MoabSim(Action: SimAction, Config: SimConfig): SimState {
116 | # Automatically launch the simulator with this
117 | # registered package name.
118 | package "Moab"
119 | }
120 |
121 | training {
122 | # Limit episodes to 250 iterations instead of the default 1000.
123 | EpisodeIterationLimit: 250
124 | }
125 |
126 | # Specify the state transformation function used when training or assessing using the simulator.
127 | # This transform is _not_ included when the brain is exported — it is only used in training and assessment.
128 | state TransformState
129 |
130 | # The objective of training is expressed as a goal with two
131 | # subgoals: don't let the ball fall off the plate, and drive
132 | # the ball to the center of the plate.
133 | goal (State: SimState) {
134 | avoid `Fall Off Plate`: Math.Hypot(State.ball_x, State.ball_y) in Goal.RangeAbove(RadiusOfPlate * 0.8)
135 | drive `Center Of Plate`: [State.ball_x, State.ball_y] in Goal.Sphere([0, 0], CloseEnough)
136 | }
137 |
138 | lesson `Randomize Start` {
139 | # Specify the configuration parameters that should be varied
140 | # from one episode to the next during this lesson.
141 | scenario {
142 | initial_x: number<-RadiusOfPlate * 0.5 .. RadiusOfPlate * 0.5>,
143 | initial_y: number<-RadiusOfPlate * 0.5 .. RadiusOfPlate * 0.5>,
144 |
145 | initial_vel_x: number<-MaxInitialVelocity * 0.02 .. MaxInitialVelocity * 0.02>,
146 | initial_vel_y: number<-MaxInitialVelocity * 0.02 .. MaxInitialVelocity * 0.02>,
147 |
148 | initial_pitch: number<-0.2 .. 0.2>,
149 | initial_roll: number<-0.2 .. 0.2>,
150 |
151 | target_x: 0,
152 | target_y: 0,
153 | }
154 | }
155 | }
156 | }
157 | }
158 |
159 | # Special string to hook up the simulator visualizer
160 | # in the web interface.
161 | const SimulatorVisualizer = "/moabviz/"
162 |
--------------------------------------------------------------------------------
/Machine-Teaching-Examples/moab_stochastic.ink:
--------------------------------------------------------------------------------
1 | ###
2 |
3 | # MSFT Bonsai
4 | # Copyright 2021 Microsoft
5 | # This code is licensed under MIT license (see LICENSE for details)
6 |
7 | # Moab example demonstrating AI learning a general policy despite being
8 | # stochastic because noise is added to the actuator for plate angles.
9 | # The noise is a gaussian with a mean of zero and a sigma of scalar/3
10 |
11 | ###
12 |
13 | inkling "2.0"
14 |
15 | using Math
16 | using Goal
17 |
18 | # Distances measured in meters
19 | const RadiusOfPlate = 0.1125 # m
20 |
21 | # Velocities measured in meters per sec.
22 | const MaxVelocity = 6.0
23 | const MaxInitialVelocity = 1.0
24 |
25 | # Threshold for ball placement
26 | const CloseEnough = 0.02
27 |
28 | # Default time delta between simulation steps (s)
29 | const DefaultTimeDelta = 0.045
30 |
31 | # Maximum distance per step in meters
32 | const MaxDistancePerStep = DefaultTimeDelta * MaxVelocity
33 |
34 | # State received from the simulator after each iteration
35 | type SimState {
36 | # Ball X,Y position
37 | ball_x: number<-MaxDistancePerStep - RadiusOfPlate .. RadiusOfPlate + MaxDistancePerStep>,
38 | ball_y: number<-MaxDistancePerStep - RadiusOfPlate .. RadiusOfPlate + MaxDistancePerStep>,
39 |
40 | # Ball X,Y velocity
41 | ball_vel_x: number<-MaxVelocity .. MaxVelocity>,
42 | ball_vel_y: number<-MaxVelocity .. MaxVelocity>,
43 |
44 | # Actual plate theta X, Y
45 | plate_theta_x: number,
46 | plate_theta_y: number
47 | }
48 |
49 | # State to send to the brain with simulated noise
50 | type ObservableState {
51 | # Ball X,Y position
52 | ball_x: number<-MaxDistancePerStep - RadiusOfPlate .. RadiusOfPlate + MaxDistancePerStep>,
53 | ball_y: number<-MaxDistancePerStep - RadiusOfPlate .. RadiusOfPlate + MaxDistancePerStep>,
54 |
55 | # Ball X,Y velocity
56 | ball_vel_x: number<-MaxVelocity .. MaxVelocity>,
57 | ball_vel_y: number<-MaxVelocity .. MaxVelocity>,
58 | }
59 |
60 | # Action provided as output by policy and sent as
61 | # input to the simulator
62 | type SimAction {
63 | # Range -1 to 1 is a scaled value that represents
64 | # the full plate rotation range supported by the hardware.
65 | input_pitch: number<-1 .. 1>, # rotate about x-axis
66 | input_roll: number<-1 .. 1>, # rotate about y-axis
67 | }
68 |
69 | # Per-episode configuration that can be sent to the simulator.
70 | # All iterations within an episode will use the same configuration.
71 | type SimConfig {
72 | # Model initial ball conditions
73 | initial_x: number<-RadiusOfPlate .. RadiusOfPlate>, # in (m)
74 | initial_y: number<-RadiusOfPlate .. RadiusOfPlate>,
75 |
76 | # Model initial ball velocity conditions
77 | initial_vel_x: number<-MaxInitialVelocity .. MaxInitialVelocity>, # in (m/s)
78 | initial_vel_y: number<-MaxInitialVelocity .. MaxInitialVelocity>,
79 |
80 | # Range -1 to 1 is a scaled value that represents
81 | # the full plate rotation range supported by the hardware.
82 | initial_pitch: number<-1 .. 1>,
83 | initial_roll: number<-1 .. 1>,
84 |
85 | # Add actuator noise to plate angles
86 | plate_noise: number,
87 | }
88 |
89 | # Define a concept graph with a single concept
90 | graph (input: ObservableState) {
91 | concept MoveToCenter(input): SimAction {
92 | curriculum {
93 | # The source of training for this concept is a simulator that
94 | # - can be configured for each episode using fields defined in SimConfig,
95 | # - accepts per-iteration actions defined in SimAction, and
96 | # - outputs states with the fields defined in SimState.
97 | source simulator MoabSim (Action: SimAction, Config: SimConfig): SimState {
98 | }
99 |
100 | # The objective of training is expressed as a goal with two
101 | # subgoals: don't let the ball fall off the plate, and drive
102 | # the ball to the center of the plate.
103 | goal (State: SimState) {
104 | avoid `Fall Off Plate`:
105 | Math.Hypot(State.ball_x, State.ball_y) in Goal.RangeAbove(RadiusOfPlate * 0.8)
106 | drive `Center Of Plate`:
107 | [State.ball_x, State.ball_y] in Goal.Sphere([0, 0], CloseEnough)
108 | }
109 |
110 | training {
111 | # Limit episodes to 250 iterations instead of the default 1000.
112 | EpisodeIterationLimit: 250
113 | }
114 |
115 | lesson `Randomize Start` {
116 | # Specify the configuration parameters that should be varied
117 | # from one episode to the next during this lesson.
118 | scenario {
119 | initial_x: number<-RadiusOfPlate * 0.5 .. RadiusOfPlate * 0.5>,
120 | initial_y: number<-RadiusOfPlate * 0.5 .. RadiusOfPlate * 0.5>,
121 |
122 | initial_vel_x: number<-MaxInitialVelocity * 0.02 .. MaxInitialVelocity * 0.02>,
123 | initial_vel_y: number<-MaxInitialVelocity * 0.02 .. MaxInitialVelocity * 0.02>,
124 |
125 | initial_pitch: number<-0.2 .. 0.2>,
126 | initial_roll: number<-0.2 .. 0.2>,
127 |
128 | plate_noise: 0.017, # ~1 degree max
129 | }
130 | }
131 | }
132 | }
133 | }
134 |
135 | # Special string to hook up the simulator visualizer
136 | # in the web interface.
137 | const SimulatorVisualizer = "/moabviz/"
--------------------------------------------------------------------------------
/Machine-Teaching-Examples/moab_with_noise.ink:
--------------------------------------------------------------------------------
1 | ###
2 |
3 | # MSFT Bonsai
4 | # Copyright 2021 Microsoft
5 | # This code is licensed under MIT license (see LICENSE for details)
6 |
7 | # Moab example demonstrating AI learning a general policy despite noise
8 | # added to the simulated sensors and plate angles. The noise is a gaussian
9 | # with a mean of zero and a sigma of scalar/3
10 |
11 | ###
12 |
13 | inkling "2.0"
14 |
15 | using Math
16 | using Goal
17 |
18 | # Distances measured in meters
19 | const RadiusOfPlate = 0.1125 # m
20 |
21 | # Velocities measured in meters per sec.
22 | const MaxVelocity = 6.0
23 | const MaxInitialVelocity = 1.0
24 |
25 | # Threshold for ball placement
26 | const CloseEnough = 0.02
27 |
28 | # Default time delta between simulation steps (s)
29 | const DefaultTimeDelta = 0.045
30 |
31 | # Maximum distance per step in meters
32 | const MaxDistancePerStep = DefaultTimeDelta * MaxVelocity
33 |
34 | # State received from the simulator after each iteration
35 | type SimState {
36 | # Ball X,Y position
37 | ball_x: number<-MaxDistancePerStep - RadiusOfPlate .. RadiusOfPlate + MaxDistancePerStep>,
38 | ball_y: number<-MaxDistancePerStep - RadiusOfPlate .. RadiusOfPlate + MaxDistancePerStep>,
39 |
40 | # Ball X,Y velocity
41 | ball_vel_x: number<-MaxVelocity .. MaxVelocity>,
42 | ball_vel_y: number<-MaxVelocity .. MaxVelocity>,
43 |
44 | # Estimated Ball X,Y position
45 | estimated_x: number<-MaxDistancePerStep - RadiusOfPlate .. RadiusOfPlate + MaxDistancePerStep>,
46 | estimated_y: number<-MaxDistancePerStep - RadiusOfPlate .. RadiusOfPlate + MaxDistancePerStep>,
47 |
48 | # Estimated Ball X,Y velocity
49 | estimated_vel_x: number<-MaxVelocity .. MaxVelocity>,
50 | estimated_vel_y: number<-MaxVelocity .. MaxVelocity>,
51 | }
52 |
53 | # State to send to the brain with simulated noise
54 | type ObservableState {
55 | # Ball X,Y position
56 | estimated_x: number<-MaxDistancePerStep - RadiusOfPlate .. RadiusOfPlate + MaxDistancePerStep>,
57 | estimated_y: number<-MaxDistancePerStep - RadiusOfPlate .. RadiusOfPlate + MaxDistancePerStep>,
58 |
59 | # Ball X,Y velocity
60 | estimated_vel_x: number<-MaxVelocity .. MaxVelocity>,
61 | estimated_vel_y: number<-MaxVelocity .. MaxVelocity>,
62 | }
63 |
64 | # Action provided as output by policy and sent as
65 | # input to the simulator
66 | type SimAction {
67 | # Range -1 to 1 is a scaled value that represents
68 | # the full plate rotation range supported by the hardware.
69 | input_pitch: number<-1 .. 1>, # rotate about x-axis
70 | input_roll: number<-1 .. 1>, # rotate about y-axis
71 | }
72 |
73 | # Per-episode configuration that can be sent to the simulator.
74 | # All iterations within an episode will use the same configuration.
75 | type SimConfig {
76 | # Model initial ball conditions
77 | initial_x: number<-RadiusOfPlate .. RadiusOfPlate>, # in (m)
78 | initial_y: number<-RadiusOfPlate .. RadiusOfPlate>,
79 |
80 | # Model initial ball velocity conditions
81 | initial_vel_x: number<-MaxInitialVelocity .. MaxInitialVelocity>, # in (m/s)
82 | initial_vel_y: number<-MaxInitialVelocity .. MaxInitialVelocity>,
83 |
84 | # Range -1 to 1 is a scaled value that represents
85 | # the full plate rotation range supported by the hardware.
86 | initial_pitch: number<-1 .. 1>,
87 | initial_roll: number<-1 .. 1>,
88 |
89 | # Add sensor noise to the states
90 | ball_noise: number,
91 | }
92 |
93 | # Define a concept graph with a single concept
94 | graph (input: ObservableState) {
95 | concept MoveToCenter(input): SimAction {
96 | curriculum {
97 | # The source of training for this concept is a simulator that
98 | # - can be configured for each episode using fields defined in SimConfig,
99 | # - accepts per-iteration actions defined in SimAction, and
100 | # - outputs states with the fields defined in SimState.
101 | source simulator MoabSim (Action: SimAction, Config: SimConfig): SimState {
102 | }
103 |
104 | # The objective of training is expressed as a goal with two
105 | # subgoals: don't let the ball fall off the plate, and drive
106 | # the ball to the center of the plate.
107 | goal (State: SimState) {
108 | avoid `Fall Off Plate`:
109 | Math.Hypot(State.ball_x, State.ball_y) in Goal.RangeAbove(RadiusOfPlate * 0.8)
110 | drive `Center Of Plate`:
111 | [State.ball_x, State.ball_y] in Goal.Sphere([0, 0], CloseEnough)
112 | }
113 |
114 | training {
115 | # Limit episodes to 250 iterations instead of the default 1000.
116 | EpisodeIterationLimit: 250
117 | }
118 |
119 | lesson `Randomize Start` {
120 | # Specify the configuration parameters that should be varied
121 | # from one episode to the next during this lesson.
122 | scenario {
123 | initial_x: number<-RadiusOfPlate * 0.5 .. RadiusOfPlate * 0.5>,
124 | initial_y: number<-RadiusOfPlate * 0.5 .. RadiusOfPlate * 0.5>,
125 |
126 | initial_vel_x: number<-MaxInitialVelocity * 0.02 .. MaxInitialVelocity * 0.02>,
127 | initial_vel_y: number<-MaxInitialVelocity * 0.02 .. MaxInitialVelocity * 0.02>,
128 |
129 | initial_pitch: number<-0.2 .. 0.2>,
130 | initial_roll: number<-0.2 .. 0.2>,
131 |
132 | ball_noise: 0.010, # ~10 mm max
133 | }
134 | }
135 | }
136 | }
137 | }
138 |
139 | # Special string to hook up the simulator visualizer
140 | # in the web interface.
141 | const SimulatorVisualizer = "/moabviz/"
--------------------------------------------------------------------------------
/Machine-Teaching-Examples/model_import/LAW.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/microsoft/moabsim-py/54bb72bf47f6c23607f910d0ba352c07f0a7ba73/Machine-Teaching-Examples/model_import/LAW.png
--------------------------------------------------------------------------------
/Machine-Teaching-Examples/model_import/README.md:
--------------------------------------------------------------------------------
1 | # How To: build and import a tensorflow-keras model in bonsai
2 |
3 | This sample illustrates how to import a Machine Learning [ML] model in Bonsai:
4 | 1. We build a ML model using tensorflow-keras
5 | 1. note: other frameworks such as pure Tensorflow are supported, in this sample we focus on Keras
6 | 2. We illustrate by building a dummy deep transform:
7 | 1. A Keras model (deep neural net) that transforms the input by applying a non linear transformation
8 |
9 | 2. Save the ML model as either
10 | 1. `tensorflow SavedModel` format OR
11 | 2. `onnx` format
12 | 3. Import the saved model to the Bonsai platform using `bonsai-cli`
13 | 4. Use it in the Moab example.
14 |
15 |
16 |
17 | ## Requirements
18 | 1. Create a conda virtual environment using the `environment.yml`.
19 | `conda env create -f environment.yml`
20 | 2. Currently only ML models compatible with tensorflow v1.15.2 are supported [(Link to docs)](https://docs.microsoft.com/en-us/bonsai/guides/import-ml-models?tabs=onnx%2Cnested1).
21 | 3. When saving a Keras model as a Tensorflow SavedModel, one needs to save it using `tf.saved_model.builder.SavedModelBuilder()`. Saving a Keras model using the direct `model.save()` is currently incompatible with Bonsai model import. (see *state_transform_keras_model.ipynb* Jupyter notebook)
22 |
23 | ## Usage
24 | 1. Using the *state_transform_keras_model.ipynb* notebook
25 | 1. Build a tensorflow-keras model
26 | 2. Save the ML model as `tensorflow SavedModel` format OR `onnx` format
27 | 2. Compressing the model as a `.zip`
28 | 1. If the ML model is saved as a `tensorflow SavedModel`, compress the saved_model folder as a `.zip`
29 | 2. If the ML model is saved as `.onnx`, there is no need to compress as `.zip`
30 | 3. Import the ML model to bonsai
31 | 1. `tensorflow SavedModel` format OR
32 | ```
33 | bonsai importedmodel create
34 | --name
35 | --modelfilepath ./state_transform_deep.zip
36 | --description "state transform tf SavedModel"
37 | --display-name
38 | ```
39 | 2. `onnx` format
40 | ```
41 | bonsai importedmodel create
42 | --name
43 | --modelfilepath ./state_transform_deep.onnx
44 | --description "state transform ONNX"
45 | --display-name
46 | ```
47 | 1. Use the imported model to train your brain using a Moab sample
48 | 1. On preview.bons.ai
49 | 1. Create a new brain from a "Moab sample"
50 | 2. Copy paste the inkling from *moab-imported-concept.ink* in the *Teach* panel UI. In this sample we use the imported ML model to transform the state before passing it as a brain input (see inkling excerpt below).
51 | ```
52 | graph (input: ObservableState) {
53 | concept ImportedConcept(input): ObservableState {
54 | import {Model: }
55 | }
56 | concept MoveToCenter(ImportedConcept): SimAction {
57 | ...
58 | }
59 | output MoveToCenter
60 | }
61 | ```
62 | 2. Build the ML model as an imported concept named ImportedConcept: click `Build ImportedConcept`
63 | 3. Trained the brain: click `Train MoveToCenter` (see below for an expected brain training graph)
64 | 
65 |
66 | ## Pytest
67 |
68 | Run a pytest to import a model, build the concept, train moab with the model as input, use custom assessment, and assert performance from logs from your Log Analytics Workspace.
69 |
70 | ### Prerequisites
71 |
72 | - Bonsai Workspace
73 | - Moab managed sim already in workspace (create a brain using the sample)
74 | - azure cli
75 | - docker
76 | - bonsai-cli
77 |
78 | ```bash
79 | pytest tests/test_model_import.py -s \
80 | --brain_name \
81 | --log_analy_workspace \
82 | --custom_assess_name \
83 | --model_file_path "./Machine-Teaching-Examples/model_import/state_transform_deep.zip"
84 |
85 | or
86 | --model_file_path "./Machine-Teaching-Examples/model_import/state_transform_deep.onnx"
87 | ```
88 |
89 | Be sure to use your `workspace id` and not your `workspace name` in your Log Analytics Workspace within Azure Portal.
90 |
91 | 
--------------------------------------------------------------------------------
/Machine-Teaching-Examples/model_import/environment.yml:
--------------------------------------------------------------------------------
1 | name: model-import
2 | dependencies:
3 | - python=3.7
4 | - pip=19.1.1
5 | - pip:
6 | - microsoft-bonsai-api
7 | - bonsai-cli
8 | - matplotlib
9 | - scipy
10 | - numpy
11 | - tensorflow==1.15.2
12 | - keras2onnx==1.7.0
13 | - cryptography==2.9.2
14 | - keras==2.3.1
15 | - jupyter==1.0.0
16 | - pandas==1.1.2
17 | - azure-loganalytics==0.1.0
18 | - azure-mgmt-loganalytics==0.7.0
19 | - pytest
20 | - azure-cli==2.10.1
--------------------------------------------------------------------------------
/Machine-Teaching-Examples/model_import/imported-concept-graph.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/microsoft/moabsim-py/54bb72bf47f6c23607f910d0ba352c07f0a7ba73/Machine-Teaching-Examples/model_import/imported-concept-graph.png
--------------------------------------------------------------------------------
/Machine-Teaching-Examples/model_import/moab-imported-concept.ink:
--------------------------------------------------------------------------------
1 | ###
2 |
3 | # MSFT Bonsai
4 | # Copyright 2021 Microsoft
5 | # This code is licensed under MIT license (see LICENSE for details)
6 |
7 | # Moab sample illustrating how to import a pre-trained neural network
8 | # model in a Bonsai brain. Use the Jupyter notebook to
9 | # create a dummy deep state transform model in either Tensorflow v1.15.2
10 | # SavedModelBuilder or ONNX format.
11 |
12 | # Use the bonsai-cli to import the model to Bonsai:
13 |
14 | # bonsai importedmodel create
15 | # --name
16 | # --modelfilepath
17 | # --description "state transform NN"
18 | # --display-name
19 |
20 | # Where:
21 | # might be state_transform_deep.zip or state_transform_deep.onnx
22 | # might be "My ML Model"
23 |
24 | ###
25 |
26 | inkling "2.0"
27 |
28 | using Math
29 | using Goal
30 |
31 | # Distances measured in meters
32 | const RadiusOfPlate = 0.1125 # m
33 |
34 | # Velocities measured in meters per sec.
35 | const MaxVelocity = 6.0
36 | const MaxInitialVelocity = 1.0
37 |
38 | # Threshold for ball placement
39 | const CloseEnough = 0.02
40 |
41 | # Default time delta between simulation steps (s)
42 | const DefaultTimeDelta = 0.045
43 |
44 | # Maximum distance per step in meters
45 | const MaxDistancePerStep = DefaultTimeDelta * MaxVelocity
46 |
47 | # State received from the simulator after each iteration
48 |
49 | type ObservableState {
50 | # Ball X,Y position
51 | ball_x: number<-MaxDistancePerStep - RadiusOfPlate .. RadiusOfPlate + MaxDistancePerStep>,
52 | ball_y: number<-MaxDistancePerStep - RadiusOfPlate .. RadiusOfPlate + MaxDistancePerStep>,
53 |
54 | # Ball X,Y velocity
55 | ball_vel_x: number<-MaxVelocity .. MaxVelocity>,
56 | ball_vel_y: number<-MaxVelocity .. MaxVelocity>,
57 |
58 | # Testing added sim observables
59 | estimated_x: number<-MaxDistancePerStep - RadiusOfPlate .. RadiusOfPlate + MaxDistancePerStep>,
60 | estimated_y:number<-MaxDistancePerStep - RadiusOfPlate .. RadiusOfPlate + MaxDistancePerStep>,
61 | }
62 |
63 | # Action provided as output by policy and sent as
64 | # input to the simulator
65 | type SimAction {
66 | # Range -1 to 1 is a scaled value that represents
67 | # the full plate rotation range supported by the hardware.
68 | input_pitch: number<-1 .. 1>, # rotate about x-axis
69 | input_roll: number<-1 .. 1>, # rotate about y-axis
70 | }
71 |
72 | # Per-episode configuration that can be sent to the simulator.
73 | # All iterations within an episode will use the same configuration.
74 | type SimConfig {
75 | # Model initial ball conditions
76 | initial_x: number<-RadiusOfPlate .. RadiusOfPlate>, # in (m)
77 | initial_y: number<-RadiusOfPlate .. RadiusOfPlate>,
78 |
79 | # Model initial ball velocity conditions
80 | initial_vel_x: number<-MaxInitialVelocity .. MaxInitialVelocity>, # in (m/s)
81 | initial_vel_y: number<-MaxInitialVelocity .. MaxInitialVelocity>,
82 |
83 | # Range -1 to 1 is a scaled value that represents
84 | # the full plate rotation range supported by the hardware.
85 | initial_pitch: number<-1 .. 1>,
86 | initial_roll: number<-1 .. 1>,
87 | }
88 |
89 |
90 |
91 | # Define a concept graph with a single concept
92 | graph (input: ObservableState) {
93 |
94 | # Add the imported concept by name with the correct type definitions
95 | # - can only have one input
96 | # - cannot use image inputs
97 | # - must have an input with the same dimensions as the Inkling concept
98 | # it maps to
99 | concept ImportedConcept(input): ObservableState {
100 | import {Model: "My ML Model"}
101 | }
102 |
103 | concept MoveToCenter(ImportedConcept): SimAction {
104 | curriculum {
105 | # The source of training for this concept is a simulator that
106 | # - can be configured for each episode using fields defined in SimConfig,
107 | # - accepts per-iteration actions defined in SimAction, and
108 | # - outputs states with the fields defined in SimState.
109 | source simulator MoabSim(Action: SimAction, Config: SimConfig): ObservableState {
110 | # Automatically launch the simulator with this
111 | # registered package name.
112 | package "Moab"
113 | }
114 |
115 | training {
116 | # Limit episodes to 250 iterations instead of the default 1000.
117 | EpisodeIterationLimit: 250
118 | }
119 |
120 | # The objective of training is expressed as a goal with two
121 | # subgoals: don't let the ball fall off the plate, and drive
122 | # the ball to the center of the plate.
123 | goal (State: ObservableState) {
124 | avoid `Fall Off Plate`:
125 | Math.Hypot(State.ball_x, State.ball_y)
126 | in Goal.RangeAbove(RadiusOfPlate * 0.8)
127 | drive `Center Of Plate`:
128 | [State.ball_x, State.ball_y]
129 | in Goal.Sphere([0, 0], CloseEnough)
130 | }
131 |
132 | lesson `Randomize Start` {
133 | # Specify the configuration parameters that should be varied
134 | # from one episode to the next during this lesson.
135 | scenario {
136 | initial_x: number<-RadiusOfPlate * 0.5 .. RadiusOfPlate * 0.5>,
137 | initial_y: number<-RadiusOfPlate * 0.5 .. RadiusOfPlate * 0.5>,
138 |
139 | initial_vel_x: number<-MaxInitialVelocity * 0.02 .. MaxInitialVelocity * 0.02>,
140 | initial_vel_y: number<-MaxInitialVelocity * 0.02 .. MaxInitialVelocity * 0.02>,
141 |
142 | initial_pitch: number<-0.2 .. 0.2>,
143 | initial_roll: number<-0.2 .. 0.2>,
144 | }
145 | }
146 | }
147 | }
148 | output MoveToCenter
149 | }
150 |
151 | # Special string to hook up the simulator visualizer
152 | # in the web interface.
153 | const SimulatorVisualizer = "/moabviz/"
154 |
--------------------------------------------------------------------------------
/Machine-Teaching-Examples/model_import/state_transform_deep.onnx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/microsoft/moabsim-py/54bb72bf47f6c23607f910d0ba352c07f0a7ba73/Machine-Teaching-Examples/model_import/state_transform_deep.onnx
--------------------------------------------------------------------------------
/Machine-Teaching-Examples/model_import/state_transform_deep.zip:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/microsoft/moabsim-py/54bb72bf47f6c23607f910d0ba352c07f0a7ba73/Machine-Teaching-Examples/model_import/state_transform_deep.zip
--------------------------------------------------------------------------------
/Machine-Teaching-Examples/model_import/state_transform_keras_model.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "markdown",
5 | "metadata": {
6 | "id": "MhoQ0WE77laV"
7 | },
8 | "source": [
9 | "## Goal: Create a Keras Machine Learning Model, Save it as either SavedModel or Onnx"
10 | ]
11 | },
12 | {
13 | "cell_type": "code",
14 | "execution_count": 8,
15 | "metadata": {
16 | "execution": {
17 | "iopub.execute_input": "2021-05-13T01:28:03.750807Z",
18 | "iopub.status.busy": "2021-05-13T01:28:03.750218Z",
19 | "iopub.status.idle": "2021-05-13T01:28:09.430878Z",
20 | "shell.execute_reply": "2021-05-13T01:28:09.431273Z"
21 | },
22 | "id": "dzLKpmZICaWN"
23 | },
24 | "outputs": [
25 | {
26 | "output_type": "stream",
27 | "name": "stdout",
28 | "text": [
29 | "1.15.2\n"
30 | ]
31 | }
32 | ],
33 | "source": [
34 | "# TensorFlow and tf.keras\n",
35 | "import tensorflow as tf\n",
36 | "\n",
37 | "# Helper libraries\n",
38 | "import numpy as np\n",
39 | "import matplotlib.pyplot as plt\n",
40 | "\n",
41 | "print(tf.__version__)"
42 | ]
43 | },
44 | {
45 | "cell_type": "markdown",
46 | "metadata": {
47 | "id": "Gxg1XGm0eOBy"
48 | },
49 | "source": [
50 | "## Set up the layers of your keras model\n",
51 | "In this example we setup a deep neural net. We won't seek to train the model.\n"
52 | ]
53 | },
54 | {
55 | "cell_type": "code",
56 | "execution_count": 13,
57 | "metadata": {
58 | "execution": {
59 | "iopub.execute_input": "2021-05-13T01:28:12.959246Z",
60 | "iopub.status.busy": "2021-05-13T01:28:12.958646Z",
61 | "iopub.status.idle": "2021-05-13T01:28:14.576221Z",
62 | "shell.execute_reply": "2021-05-13T01:28:14.576664Z"
63 | },
64 | "id": "9ODch-OFCaW4"
65 | },
66 | "outputs": [
67 | {
68 | "output_type": "stream",
69 | "name": "stdout",
70 | "text": [
71 | "Model: \"sequential_4\"\n_________________________________________________________________\nLayer (type) Output Shape Param # \n=================================================================\nlambda_4 (Lambda) (None, 6) 0 \n_________________________________________________________________\ndense_12 (Dense) (None, 4) 28 \n_________________________________________________________________\ndense_13 (Dense) (None, 4) 20 \n_________________________________________________________________\ndense_14 (Dense) (None, 6) 30 \n=================================================================\nTotal params: 78\nTrainable params: 78\nNon-trainable params: 0\n_________________________________________________________________\n"
72 | ]
73 | }
74 | ],
75 | "source": [
76 | "input_shape = (6,)\n",
77 | "model = tf.keras.Sequential([\n",
78 | " tf.keras.Input(shape = input_shape),\n",
79 | " tf.keras.layers.Lambda(lambda x: x*2), #trivial operation multiplying all inputs by a factor 2\n",
80 | " tf.keras.layers.Dense(4), #comment out if you want to build a trivial model that only multiplies input by 2\n",
81 | " tf.keras.layers.Dense(4), #comment out if you want to build a trivial model that only multiplies input by 2\n",
82 | " tf.keras.layers.Dense(6), #comment out if you want to build a trivial model that only multiplies input by 2\n",
83 | "])\n",
84 | "\n",
85 | "model.summary()"
86 | ]
87 | },
88 | {
89 | "cell_type": "code",
90 | "execution_count": 14,
91 | "metadata": {},
92 | "outputs": [
93 | {
94 | "output_type": "execute_result",
95 | "data": {
96 | "text/plain": [
97 | "(1, 6)"
98 | ]
99 | },
100 | "metadata": {},
101 | "execution_count": 14
102 | }
103 | ],
104 | "source": [
105 | "# creating a test input (ones)\n",
106 | "x = np.array([1,1,1,1,1,1])\n",
107 | "x = np.reshape(x, (1,6))\n",
108 | "x.shape"
109 | ]
110 | },
111 | {
112 | "cell_type": "code",
113 | "execution_count": 15,
114 | "metadata": {},
115 | "outputs": [
116 | {
117 | "output_type": "execute_result",
118 | "data": {
119 | "text/plain": [
120 | "array([[ 0.911648 , 2.2982535 , -0.77847415, -1.4436646 , -1.3750376 ,\n",
121 | " -0.7393315 ]], dtype=float32)"
122 | ]
123 | },
124 | "metadata": {},
125 | "execution_count": 15
126 | }
127 | ],
128 | "source": [
129 | "# testing the model with a test input\n",
130 | "model.predict(x) "
131 | ]
132 | },
133 | {
134 | "source": [
135 | "## Save model for bonsai model import\n",
136 | "1. tf saved model format using SavedModelBuilder\n",
137 | "2. onnx saved model\n",
138 | "3. NOT COMPATIBLE with bonsai model import: direct tf SavedModel"
139 | ],
140 | "cell_type": "markdown",
141 | "metadata": {}
142 | },
143 | {
144 | "cell_type": "code",
145 | "execution_count": 16,
146 | "metadata": {},
147 | "outputs": [
148 | {
149 | "output_type": "stream",
150 | "name": "stdout",
151 | "text": [
152 | "builder\n",
153 | "INFO:tensorflow:No assets to save.\n",
154 | "INFO:tensorflow:No assets to write.\n",
155 | "INFO:tensorflow:SavedModel written to: ./state_transform_deep/saved_model.pb\n"
156 | ]
157 | },
158 | {
159 | "output_type": "execute_result",
160 | "data": {
161 | "text/plain": [
162 | "b'./state_transform_deep/saved_model.pb'"
163 | ]
164 | },
165 | "metadata": {},
166 | "execution_count": 16
167 | }
168 | ],
169 | "source": [
170 | "#save keras model using SavedModelBuilder: compress as a .zip and ready for bonsai model import\n",
171 | "from keras import backend as K\n",
172 | "import tensorflow as tf\n",
173 | "signature = tf.saved_model.signature_def_utils.predict_signature_def(\n",
174 | " inputs={\"data\": model.input}, outputs={\"out\": model.output}\n",
175 | ")\n",
176 | "MODEL_PATH_SAVEDMODEL = \"./state_transform_deep\"\n",
177 | "builder = tf.saved_model.builder.SavedModelBuilder(MODEL_PATH_SAVEDMODEL)\n",
178 | "print('builder')\n",
179 | "builder.add_meta_graph_and_variables(\n",
180 | " sess=K.get_session(),\n",
181 | " tags=[tf.saved_model.tag_constants.SERVING],\n",
182 | " signature_def_map={\n",
183 | " tf.saved_model.signature_constants.DEFAULT_SERVING_SIGNATURE_DEF_KEY: signature\n",
184 | " },\n",
185 | ")\n",
186 | "builder.save()"
187 | ]
188 | },
189 | {
190 | "cell_type": "code",
191 | "execution_count": 17,
192 | "metadata": {},
193 | "outputs": [
194 | {
195 | "output_type": "stream",
196 | "name": "stderr",
197 | "text": [
198 | "The ONNX operator number change on the optimization: 14 -> 7\n",
199 | "The maximum opset needed by this model is only 9.\n"
200 | ]
201 | }
202 | ],
203 | "source": [
204 | "#save keras model as ONNX: ready for bonsai model import\n",
205 | "import os\n",
206 | "os.environ['TF_KERAS'] = '1'\n",
207 | "import keras2onnx\n",
208 | "onnx_model = keras2onnx.convert_keras(model, model.name)\n",
209 | "\n",
210 | "with open(\"state_transform_deep.onnx\", \"wb\") as f:\n",
211 | " f.write(onnx_model.SerializeToString())"
212 | ]
213 | },
214 | {
215 | "cell_type": "code",
216 | "execution_count": null,
217 | "metadata": {},
218 | "outputs": [],
219 | "source": [
220 | "#CAUTION this way of saving tf SavedModel is not be compatible with bonsai model import\n",
221 | "#save keras model as tf savedmodel\n",
222 | "#model.save('saved_model/my_model',save_format='SavedModel')"
223 | ]
224 | }
225 | ],
226 | "metadata": {
227 | "colab": {
228 | "collapsed_sections": [],
229 | "name": "classification.ipynb",
230 | "toc_visible": true
231 | },
232 | "kernelspec": {
233 | "name": "python3710jvsc74a57bd0e7e0ed120b68561b3ba35405b40e60bc605557aa4fbc88a8a2ecdcf4993dcd76",
234 | "display_name": "Python 3.7.10 64-bit ('model-import': conda)"
235 | },
236 | "language_info": {
237 | "codemirror_mode": {
238 | "name": "ipython",
239 | "version": 3
240 | },
241 | "file_extension": ".py",
242 | "mimetype": "text/x-python",
243 | "name": "python",
244 | "nbconvert_exporter": "python",
245 | "pygments_lexer": "ipython3",
246 | "version": "3.7.10"
247 | },
248 | "metadata": {
249 | "interpreter": {
250 | "hash": "d414d662c8e6e82f50b7fda920b17dedcb1a2bc687fc6867fe60c7d4063d2a99"
251 | }
252 | }
253 | },
254 | "nbformat": 4,
255 | "nbformat_minor": 0
256 | }
--------------------------------------------------------------------------------
/Machine-Teaching-Examples/model_import/training-graph.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/microsoft/moabsim-py/54bb72bf47f6c23607f910d0ba352c07f0a7ba73/Machine-Teaching-Examples/model_import/training-graph.png
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Intro
2 |
3 | Simulators need two environment variables set to be able to attach to the platform.
4 |
5 | The first is `SIM_ACCESS_KEY`. You can create one from the `Account Settings` page.
6 | You have one chance to copy the key once it has been created. Make sure you don't enter
7 | the ID.
8 |
9 | The second is `SIM_WORKSPACE`. You can find this in the URL after `/workspaces/` once
10 | you are logged in to the platform.
11 |
12 | There is also an optional `SIM_API_HOST` key, but if it is not set it will default to `https://api.bons.ai`.
13 |
14 | If you're launching your simulator from the command line, make sure that you have these two
15 | environment variables set. If you like, you could use the following example script:
16 |
17 | ```sh
18 | export SIM_WORKSPACE=
19 | export SIM_ACCESS_KEY=
20 | python3 moab_sim.py
21 | ```
22 |
23 | You will need to install support libraries prior to running. Our demos depend on `bonsai-common`.
24 | This library will need to be installed from source.
25 |
26 | ```sh
27 | pip3 install git+https://github.com/microsoft/bonsai-common
28 | ```
29 |
30 | ## Building Dockerfile
31 |
32 | docker build -t -f Dockerfile ./
33 |
34 | ## Run Dockerfile local (optional)
35 |
36 | ```
37 | docker run --rm -it -e SIM_ACCESS_KEY="" -e SIM_API_HOST="" -e SIM_WORKSPACE=""
38 | ```
39 |
40 | ## How to push to ACR
41 |
42 | ```
43 | az login (Is not necessary if you are already up to date or logged in recently)
44 | az acr login --subscription --name
45 | docker tag .azurecr.io/bonsai/
46 | docker push .azurecr.io/bonsai/
47 | ```
48 |
49 | ## Example (Assuming you logged in)
50 |
51 | ```
52 | docker build -t moab -f Dockerfile ./
53 | docker tag moab bonsaisimpreprod.azurecr.io/bonsai/moab
54 | docker push bonsaisimpreprod.azurecr.io/bonsai/moab
55 | ```
56 |
57 | ## Microsoft Open Source Code of Conduct
58 |
59 | This repository is subject to the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct).
60 |
--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | ## Security
4 |
5 | Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/).
6 |
7 | If you believe you have found a security vulnerability in any Microsoft-owned repository that meets Microsoft's [Microsoft's definition of a security vulnerability](https://docs.microsoft.com/en-us/previous-versions/tn-archive/cc751383(v=technet.10)), please report it to us as described below.
8 |
9 | ## Reporting Security Issues
10 |
11 | **Please do not report security vulnerabilities through public GitHub issues.**
12 |
13 | Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://msrc.microsoft.com/create-report).
14 |
15 | If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the the [Microsoft Security Response Center PGP Key page](https://www.microsoft.com/en-us/msrc/pgp-key-msrc).
16 |
17 | You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc).
18 |
19 | Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue:
20 |
21 | * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.)
22 | * Full paths of source file(s) related to the manifestation of the issue
23 | * The location of the affected source code (tag/branch/commit or direct URL)
24 | * Any special configuration required to reproduce the issue
25 | * Step-by-step instructions to reproduce the issue
26 | * Proof-of-concept or exploit code (if possible)
27 | * Impact of the issue, including how an attacker might exploit the issue
28 |
29 | This information will help us triage your report more quickly.
30 |
31 | If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://microsoft.com/msrc/bounty) page for more details about our active programs.
32 |
33 | ## Preferred Languages
34 |
35 | We prefer all communications to be in English.
36 |
37 | ## Policy
38 |
39 | Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://www.microsoft.com/en-us/msrc/cvd).
40 |
41 |
--------------------------------------------------------------------------------
/assess_config.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "1.0.0",
3 | "recipe": {},
4 | "episodeConfigurations": [
5 | {"ball_radius":0.02393318507670268,"initial_roll":-0.1288264770626709,"initial_vel_x":0.16900261387316273,"initial_vel_y":-0.3955991776782724,"initial_x":-0.01210678534894888,"initial_y":0.010198122189413392,"ball_shell":0.00017861655911910286,"initial_pitch":0.05904998014430585},
6 | {"initial_roll":0.10873720049858092,"initial_vel_x":0.35032984614372253,"initial_vel_y":0.17395760118961334,"initial_x":-0.05847322940826416,"initial_y":0.0018174889264628291,"ball_shell":0.0001879152114270255,"initial_pitch":-0.02228832058608532,"ball_radius":0.018350353464484215}
7 | ]
8 | }
--------------------------------------------------------------------------------
/moab.sh:
--------------------------------------------------------------------------------
1 | #! /bin/bash
2 | echo "Starting multiple moab_sim.py processes..."
3 | parallel -j 7 python3 moab_sim.py ::: {1..7}
4 |
--------------------------------------------------------------------------------
/moab_experiment.ink:
--------------------------------------------------------------------------------
1 | ###
2 |
3 | # MSFT Bonsai
4 | # Copyright 2020 Microsoft
5 |
6 | # Moab Experimental
7 | # This inkling is for exposing additional information for future experiments
8 | # for internal developers trying to improve deployments.
9 | # Traditional rewards and terminal conditions can be used if uncommented.
10 |
11 | ###
12 |
13 | inkling "2.0"
14 |
15 | using Math
16 | using Goal
17 |
18 | experiment {
19 | #auto_curriculum: "True",
20 | success_termination_threshold: "0.90",
21 | success_termination_window: "150",
22 | reward_convergence_termination_threshold: "999",
23 | }
24 |
25 | # Time constant per step
26 | const DefaultTimeDelta = 0.045 # s
27 |
28 | # Ping-Pong ball constants
29 | const PingPongRadius = 0.020 # m
30 | const PingPongShell = 0.0002 # m
31 |
32 | # Distances measured in meters
33 | const CloseEnough = 0.02
34 | const RadiusOfPlate = 0.1125
35 |
36 | # Velocities measured in meters per sec.
37 | const MaxVelocity = 3.0
38 | const MaxInitialVelocity = 0.05
39 |
40 | # Noise added to the real ball position to create the estimated ball position
41 | const DefaultBallNoise = 0.000 # m
42 |
43 | # Noise added to the commanded plate position to create the real plate position
44 | const DefaultPlateNoise = (Math.Pi / 180.0) * 0 # rad
45 |
46 | # This is the state received from the simulator
47 | # after each iteration.
48 | type SimState {
49 | # Reflected control parameters that were passed in
50 | pitch: number<-1 .. 1>,
51 | roll: number<-1 .. 1>,
52 |
53 | # Reflected episode config parameters.
54 | # See SimConfig for descriptions and units.
55 | time_delta: number,
56 | plate_theta_vel_limit: number,
57 | plate_theta_acc: number,
58 | plate_theta_limit: number,
59 | ball_noise: number,
60 | plate_noise: number,
61 |
62 | ball_radius: number,
63 | ball_shell: number,
64 |
65 | target_x: number<-RadiusOfPlate .. RadiusOfPlate>,
66 | target_y: number<-RadiusOfPlate .. RadiusOfPlate>,
67 |
68 | # Plate state used for training
69 | plate_theta_x: number,
70 | plate_theta_y: number,
71 |
72 | # Ball modelled state used for rendering
73 | ball_x: number<-RadiusOfPlate .. RadiusOfPlate>,
74 | ball_y: number<-RadiusOfPlate .. RadiusOfPlate>,
75 |
76 | ball_vel_x: number<-MaxVelocity .. MaxVelocity>,
77 | ball_vel_y: number<-MaxVelocity .. MaxVelocity>,
78 |
79 | # "Observed" ball position, an emulated estimate of what the camera sees
80 | estimated_x: number<-RadiusOfPlate .. RadiusOfPlate>,
81 | estimated_y: number<-RadiusOfPlate .. RadiusOfPlate>,
82 |
83 | estimated_vel_x: number<-MaxVelocity .. MaxVelocity>,
84 | estimated_vel_y: number<-MaxVelocity .. MaxVelocity>,
85 |
86 | ball_fell_off: number<0, 1,>,
87 | }
88 |
89 | # State that represents the input to the policy
90 | type ObservableState {
91 | # Ball X,Y position, noise applied
92 | ball_x: number<-RadiusOfPlate .. RadiusOfPlate>,
93 | ball_y: number<-RadiusOfPlate .. RadiusOfPlate>,
94 |
95 | # Ball X,Y velocity, noise applied
96 | ball_vel_x: number<-MaxVelocity .. MaxVelocity>,
97 | ball_vel_y: number<-MaxVelocity .. MaxVelocity>,
98 | }
99 |
100 | # Action that represents the output of the policy
101 | type SimAction {
102 | input_pitch: number<-1 .. 1>, # scalar over plate rotation about x-axis
103 | input_roll: number<-1 .. 1>, # scalar over plate rotation about y-axis
104 | }
105 |
106 | # Per-episode configuration that can be sent to the simulator.
107 | # All iterations within an episode will use the same configuration.
108 | type SimConfig {
109 | # Model configuration
110 | time_delta: number<0.02 .. 0.05>, # Simulation step time delta in (s)
111 | ball_noise: number, # Noise to add to real ball positions to create estimated positions (mm)
112 | plate_noise: number, # Noise, to add to the commanded plate angles to create actual plate angles (rad)
113 |
114 | ball_radius: number, # Radius of the ball in (m)
115 | ball_shell: number, # Shell thickness of ball in (m), shell>0, shell<=radius
116 |
117 | # Goal that the AI could move the ball towards
118 | target_x: number<-RadiusOfPlate .. RadiusOfPlate>,
119 | target_y: number<-RadiusOfPlate .. RadiusOfPlate>,
120 |
121 | # Model initial ball conditions
122 | initial_x: number<-RadiusOfPlate .. RadiusOfPlate>, # in (m)
123 | initial_y: number<-RadiusOfPlate .. RadiusOfPlate>,
124 |
125 | # Model initial ball velocity conditions
126 | initial_vel_x: number<-MaxInitialVelocity .. MaxInitialVelocity>, # in (m/s)
127 | initial_vel_y: number<-MaxInitialVelocity .. MaxInitialVelocity>,
128 |
129 | # Model initial plate conditions
130 | initial_pitch: number<-1 .. 1>, # scalar over full plate rotation range
131 | initial_roll: number<-1 .. 1>, # scalar over full plate rotation range
132 | }
133 |
134 | function TransformState(State: SimState): ObservableState {
135 | # Simulated sensor noise using gaussian
136 | # and skewing of the ball perception with a rotated plate using ray tracing
137 | return {
138 | ball_x: State.ball_x,
139 | ball_y: State.ball_y,
140 | ball_vel_x: State.ball_vel_x,
141 | ball_vel_y: State.ball_vel_y,
142 | }
143 | }
144 |
145 | # Reward function that is evaluated after each iteration
146 | function BalanceBallReward(State: SimState) {
147 | # Return a negative reward if the ball is off the plate
148 | # or we have hit the max iteration count for the episode.
149 | if IsBallOffPlate(State) {
150 | return -10
151 | }
152 |
153 | var DistanceToTarget = GetDistanceToTarget(State)
154 | # Agent is being rewarded based on ground truth state despite noisy sensors
155 | var Speed = Math.Hypot(State.ball_vel_x, State.ball_vel_y)
156 |
157 | if DistanceToTarget < CloseEnough {
158 | return 10
159 | }
160 |
161 | # Shape the reward.
162 | return CloseEnough / DistanceToTarget * 10
163 | }
164 |
165 | # Terminal function that is evaluated after each iteration
166 | function BalanceBallTerminal(State: SimState) {
167 | # We consider it a terminal condition when the ball
168 | # has rolled off the plate or we have hit the maximum
169 | # number of iterations.
170 | return IsBallOffPlate(State)
171 | }
172 |
173 | function IsBallOffPlate(State: SimState) {
174 | return State.ball_fell_off > 0
175 | }
176 |
177 | function GetVectorMagnitude(x: number, y: number) {
178 | return ((x ** 2) + (y ** 2)) ** 0.5
179 | }
180 |
181 | function GetDistanceToTarget(State: SimState) {
182 | var dx = State.ball_x - State.target_x
183 | var dy = State.ball_y - State.target_y
184 |
185 | return GetVectorMagnitude(dx, dy)
186 | }
187 |
188 | graph (input: ObservableState) {
189 | concept MoveToTargetLoc(input): SimAction {
190 | curriculum {
191 | source simulator (Action: SimAction, Config: SimConfig): SimState {
192 | }
193 |
194 | # avoid falling off the plate when the perceived position is near edge of tilted plate
195 | # drive ball to balance in center of plate
196 | goal (State: SimState) {
197 | avoid `Fall Off Plate`:
198 | Math.Hypot(State.ball_x, State.ball_y) in Goal.RangeAbove(RadiusOfPlate * 0.9)
199 | drive `Center Of Plate`:
200 | [State.ball_x, State.ball_y] in Goal.Sphere([0, 0], CloseEnough)
201 | }
202 |
203 | # To use reward and terminal functions instead of goals, comment
204 | # out goal statement above and uncomment the following.
205 | #reward BalanceBallReward
206 | #terminal BalanceBallTerminal
207 |
208 | # Specify transform for what info is sent to the AI
209 | state TransformState
210 |
211 | training {
212 | # Limit episodes to 250 iterations instead of the default 1000.
213 | EpisodeIterationLimit: 250,
214 | #TotalIterationLimit: 700000,
215 | #LessonSuccessThreshold: 0.90,
216 | }
217 |
218 |
219 | lesson `Lesson 1` {
220 | scenario {
221 | time_delta: DefaultTimeDelta,
222 | ball_noise: DefaultBallNoise,
223 | plate_noise: DefaultPlateNoise,
224 |
225 | ball_radius: number,
226 | ball_shell: PingPongShell,
227 |
228 | target_x: 0,
229 | target_y: 0,
230 |
231 | initial_x: number<-RadiusOfPlate * 0.636 .. RadiusOfPlate * 0.636>,
232 | initial_y: number<-RadiusOfPlate * 0.636 .. RadiusOfPlate * 0.636>,
233 |
234 | initial_vel_x: number<-0.02 .. 0.02>,
235 | initial_vel_y: number<-0.02 .. 0.02>,
236 |
237 | initial_pitch: number<-0.2 .. 0.2>,
238 | initial_roll: number<-0.2 .. 0.2>,
239 | }
240 | }
241 | }
242 | }
243 | }
244 |
245 | # Special string to hook up the simulator visualizer
246 | # in the web interface.
247 | const SimulatorVisualizer = "/moabviz/"
248 |
--------------------------------------------------------------------------------
/moab_interface.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "moab-py-v5",
3 | "timeout": 60,
4 | "description": {
5 | "config": {
6 | "category": "Struct",
7 | "fields": [
8 | {
9 | "name": "initial_pitch",
10 | "type": {
11 | "category": "Number",
12 | "defaultValue": {{initial_pitch}},
13 | "start": -1,
14 | "stop": 1,
15 | "comment": "Initial plate pitch as a control input [-1..1]"
16 | }
17 | },
18 | {
19 | "name": "initial_roll",
20 | "type": {
21 | "category": "Number",
22 | "defaultValue": {{initial_roll}},
23 | "start": -1,
24 | "stop": 1,
25 | "comment": "Initial plate roll as a control input [-1..1]"
26 | }
27 | },
28 | {
29 | "name": "initial_height_z",
30 | "type": {
31 | "category": "Number",
32 | "defaultValue": {{initial_height_z}},
33 | "start": -1,
34 | "stop": 1,
35 | "comment": "Initial plate height as a control input [-1..1]"
36 | }
37 | },
38 | {
39 | "name": "time_delta",
40 | "type": {
41 | "category": "Number",
42 | "defaultValue": {{time_delta}},
43 | "start": 0.1,
44 | "stop": 0.0083333,
45 | "comment": "Delay between simulation steps (s)"
46 | }
47 | },
48 | {
49 | "name": "jitter",
50 | "type": {
51 | "category": "Number",
52 | "defaultValue": 0.0,
53 | "start": 0.0,
54 | "stop": 1.0,
55 | "comment": "Step time jitter magnitude (s)"
56 | }
57 | },
58 | {
59 | "name": "gravity",
60 | "type": {
61 | "category": "Number",
62 | "defaultValue": {{gravity}},
63 | "comment": "Absolute gravity value (m/s**2)"
64 | }
65 | },
66 | {
67 | "name": "plate_theta_vel_limit",
68 | "type": {
69 | "category": "Number",
70 | "defaultValue": {{plate_theta_vel_limit}},
71 | "comment": "Max plate angular velocity (rad/s)"
72 | }
73 | },
74 | {
75 | "name": "plate_theta_acc",
76 | "type": {
77 | "category": "Number",
78 | "defaultValue": {{plate_theta_acc}},
79 | "comment": "Plate angular acceleration (rad/s**2)"
80 | }
81 | },
82 | {
83 | "name": "plate_theta_limit",
84 | "type": {
85 | "category": "Number",
86 | "defaultValue": {{plate_theta_limit}},
87 | "comment": "Angular rotation limit for plate, [-limit..limit] (rad)"
88 | }
89 | },
90 | {
91 | "name": "plate_z_limit",
92 | "type": {
93 | "category": "Number",
94 | "defaultValue": {{plate_z_limit}},
95 | "comment": "Plate Z-translation limit (m)"
96 | }
97 | },
98 | {
99 | "name": "ball_mass",
100 | "type": {
101 | "category": "Number",
102 | "defaultValue": {{ball_mass}},
103 | "comment": "Ball mass (kg)"
104 | }
105 | },
106 | {
107 | "name": "ball_radius",
108 | "type": {
109 | "category": "Number",
110 | "defaultValue": {{ball_radius}},
111 | "comment": "Ball radius (m)"
112 | }
113 | },
114 | {
115 | "name": "ball_shell",
116 | "type": {
117 | "category": "Number",
118 | "defaultValue": {{ball_shell}},
119 | "comment": "Ball shell thickness (m)"
120 | }
121 | },
122 | {
123 | "name": "obstacle_radius",
124 | "type": {
125 | "category": "Number",
126 | "defaultValue": {{obstacle_radius}},
127 | "comment": "Obstacle radius (m). If zero, obstacle is disabled"
128 | }
129 | },
130 | {
131 | "name": "obstacle_x",
132 | "type": {
133 | "category": "Number",
134 | "defaultValue": {{obstacle_x}},
135 | "comment": "Obstacle X position (m)"
136 | }
137 | },
138 | {
139 | "name": "obstacle_y",
140 | "type": {
141 | "category": "Number",
142 | "defaultValue": {{obstacle_y}},
143 | "comment": "Obstacle Y position (m)"
144 | }
145 | },
146 | {
147 | "name": "target_x",
148 | "type": {
149 | "category": "Number",
150 | "defaultValue": {{target_x}},
151 | "comment": "X position AI should move ball towards (m)"
152 | }
153 | },
154 | {
155 | "name": "target_y",
156 | "type": {
157 | "category": "Number",
158 | "defaultValue": {{target_y}},
159 | "comment": "Y position AI should move ball towards (m)"
160 | }
161 | },
162 | {
163 | "name": "initial_x",
164 | "type": {
165 | "category": "Number",
166 | "defaultValue": {{initial_x}},
167 | "comment": "Initial ball X position (m)"
168 | }
169 | },
170 | {
171 | "name": "initial_y",
172 | "type": {
173 | "category": "Number",
174 | "defaultValue": {{initial_y}},
175 | "comment": "Initial ball Y position (m)"
176 | }
177 | },
178 | {
179 | "name": "initial_z",
180 | "type": {
181 | "category": "Number",
182 | "defaultValue": {{initial_z}},
183 | "comment": "Initial ball Z position (m)"
184 | }
185 | },
186 | {
187 | "name": "initial_vel_x",
188 | "type": {
189 | "category": "Number",
190 | "defaultValue": {{initial_vel_x}},
191 | "comment": "Initial ball X velocity (m/s)"
192 | }
193 | },
194 | {
195 | "name": "initial_vel_y",
196 | "type": {
197 | "category": "Number",
198 | "defaultValue": {{initial_vel_y}},
199 | "comment": "Initial ball Y velocity (m/s)"
200 | }
201 | },
202 | {
203 | "name": "initial_vel_z",
204 | "type": {
205 | "category": "Number",
206 | "defaultValue": {{initial_vel_z}},
207 | "comment": "Initial ball Z velocity (m/s)"
208 | }
209 | },
210 | {
211 | "name": "initial_speed",
212 | "type": {
213 | "category": "Number",
214 | "defaultValue": {{initial_speed}},
215 | "comment": "Initial ball speed. Must set initial_direction if used or speed will be 0.0. (m/s)"
216 | }
217 | },
218 | {
219 | "name": "initial_direction",
220 | "type": {
221 | "category": "Number",
222 | "defaultValue": {{initial_direction}},
223 | "comment": "Initial ball direction, offset from vector towards target (rad)"
224 | }
225 | },
226 | {
227 | "name": "ball_noise",
228 | "type": {
229 | "category": "Number",
230 | "defaultValue": {{ball_noise}},
231 | "comment": "Magnitude of gaussian noise added to ball position in range [-noise, noise] (m)"
232 | }
233 | },
234 | {
235 | "name": "plate_noise",
236 | "type": {
237 | "category": "Number",
238 | "defaultValue": {{plate_noise}},
239 | "comment": "Magnitude of gaussian noise added to plate angle in range [-noise, noise] (rad)"
240 | }
241 | }
242 | ]
243 | },
244 | "action": {
245 | "category": "Struct",
246 | "fields": [
247 | {
248 | "name": "input_roll",
249 | "type": {
250 | "category": "Number",
251 | "start": -1,
252 | "stop": 1,
253 | "comment": "Commanded roll position"
254 | }
255 | },
256 | {
257 | "name": "input_pitch",
258 | "type": {
259 | "category": "Number",
260 | "start": -1,
261 | "stop": 1,
262 | "comment": "Commanded pitch control position"
263 | }
264 | },
265 | {
266 | "name": "input_height_z",
267 | "type": {
268 | "category": "Number",
269 | "start": -1,
270 | "stop": 1,
271 | "comment": "Starting Z plate height control value"
272 | }
273 | }
274 | ]
275 | },
276 | "state": {
277 | "category": "Struct",
278 | "fields": [
279 | {
280 | "name": "roll",
281 | "type": {
282 | "category": "Number",
283 | "start": -1,
284 | "stop": 1,
285 | "comment": "Current roll control [-1..1]"
286 | }
287 | },
288 | {
289 | "name": "pitch",
290 | "type": {
291 | "category": "Number",
292 | "start": -1,
293 | "stop": 1,
294 | "comment": "Current pitch control [-1..1]"
295 | }
296 | },
297 | {
298 | "name": "height_z",
299 | "type": {
300 | "category": "Number",
301 | "start": -1,
302 | "stop": 1,
303 | "comment": "Current Z plate height control [-1..1]"
304 | }
305 | },
306 | {
307 | "name": "elapsed_time",
308 | "type": {
309 | "category": "Number",
310 | "comment": "Time since episode start (s)"
311 | }
312 | },
313 | {
314 | "name": "time_delta",
315 | "type": {
316 | "category": "Number",
317 | "comment": "Current time delta (s)"
318 | }
319 | },
320 | {
321 | "name": "jitter",
322 | "type": {
323 | "category": "Number",
324 | "comment": "Current time jitter magnitude (s)"
325 | }
326 | },
327 | {
328 | "name": "step_time",
329 | "type": {
330 | "category": "Number",
331 | "comment": "Actual simulation step time (s). Is time_delta + noise[-jitter, jitter]"
332 | }
333 | },
334 | {
335 | "name": "gravity",
336 | "type": {
337 | "category": "Number",
338 | "comment": "Current gravity (m/s**2)"
339 | }
340 | },
341 | {
342 | "name": "plate_radius",
343 | "type": {
344 | "category": "Number",
345 | "comment": "Radius of plate (m)"
346 | }
347 | },
348 | {
349 | "name": "plate_theta_vel_limit",
350 | "type": {
351 | "category": "Number",
352 | "comment": "Current max angular velocity (rad/s)"
353 | }
354 | },
355 | {
356 | "name": "plate_theta_acc",
357 | "type": {
358 | "category": "Number",
359 | "comment": "Current angular acceleration (rad/s**2)"
360 | }
361 | },
362 | {
363 | "name": "plate_theta_limit",
364 | "type": {
365 | "category": "Number",
366 | "comment": "Current angular rotation limit [-limit..limit] (rad)"
367 | }
368 | },
369 | {
370 | "name": "plate_z_limit",
371 | "type": {
372 | "category": "Number",
373 | "comment": "Current plate z limits [-limit..limit] (m)"
374 | }
375 | },
376 | {
377 | "name": "ball_mass",
378 | "type": {
379 | "category": "Number",
380 | "comment": "Current ball mass (kg)"
381 | }
382 | },
383 | {
384 | "name": "ball_radius",
385 | "type": {
386 | "category": "Number",
387 | "comment": "Current ball radius (m)"
388 | }
389 | },
390 | {
391 | "name": "ball_shell",
392 | "type": {
393 | "category": "Number",
394 | "comment": "Current ball thickness (m)"
395 | }
396 | },
397 | {
398 | "name": "obstacle_radius",
399 | "type": {
400 | "category": "Number",
401 | "comment": "Obstacle radius (m). If zero, obstacle is disabled"
402 | }
403 | },
404 | {
405 | "name": "obstacle_x",
406 | "type": {
407 | "category": "Number",
408 | "comment": "Obstacle X position (m)"
409 | }
410 | },
411 | {
412 | "name": "obstacle_y",
413 | "type": {
414 | "category": "Number",
415 | "comment": "Obstacle Y position (m)"
416 | }
417 | },
418 | {
419 | "name": "target_x",
420 | "type": {
421 | "category": "Number",
422 | "start": -{{plate_radius}},
423 | "stop": {{plate_radius}},
424 | "comment": "Current target X position (m)"
425 | }
426 | },
427 | {
428 | "name": "target_y",
429 | "type": {
430 | "category": "Number",
431 | "start": -{{plate_radius}},
432 | "stop": {{plate_radius}},
433 | "comment": "Current target Y position (m)"
434 | }
435 | },
436 | {
437 | "name": "plate_x",
438 | "type": {
439 | "category": "Number",
440 | "comment": "Current plate X position (m)"
441 | }
442 | },
443 | {
444 | "name": "plate_y",
445 | "type": {
446 | "category": "Number",
447 | "comment": "Current plate Y position (m)"
448 | }
449 | },
450 | {
451 | "name": "plate_z",
452 | "type": {
453 | "category": "Number",
454 | "comment": "Current plate Z position (m)"
455 | }
456 | },
457 | {
458 | "name": "plate_nor_x",
459 | "type": {
460 | "category": "Number",
461 | "comment": "Current plate X normal (unitless)"
462 | }
463 | },
464 | {
465 | "name": "plate_nor_y",
466 | "type": {
467 | "category": "Number",
468 | "comment": "Current plate Y normal (unitless)"
469 | }
470 | },
471 | {
472 | "name": "plate_nor_z",
473 | "type": {
474 | "category": "Number",
475 | "comment": "Current plate Z normal (unitless)"
476 | }
477 | },
478 | {
479 | "name": "plate_theta_x",
480 | "type": {
481 | "category": "Number",
482 | "comment": "Current plate X theta angle (rad)"
483 | }
484 | },
485 | {
486 | "name": "plate_theta_y",
487 | "type": {
488 | "category": "Number",
489 | "comment": "Current plate Y theta angle (rad)"
490 | }
491 | },
492 | {
493 | "name": "plate_theta_vel_x",
494 | "type": {
495 | "category": "Number",
496 | "comment": "Current plate X theta velocity (rad/s)"
497 | }
498 | },
499 | {
500 | "name": "plate_theta_vel_y",
501 | "type": {
502 | "category": "Number",
503 | "comment": "Current plate Y theta velocity (rad/s)"
504 | }
505 | },
506 | {
507 | "name": "plate_vel_z",
508 | "type": {
509 | "category": "Number",
510 | "comment": "Current plate Z velocity (m/s)"
511 | }
512 | },
513 | {
514 | "name": "ball_x",
515 | "type": {
516 | "category": "Number",
517 | "comment": "Current ball X position ground truth (m)"
518 | }
519 | },
520 | {
521 | "name": "ball_y",
522 | "type": {
523 | "category": "Number",
524 | "comment": "Current ball Y position ground truth (m)"
525 | }
526 | },
527 | {
528 | "name": "ball_z",
529 | "type": {
530 | "category": "Number",
531 | "comment": "Current ball Z position ground truth (m)"
532 | }
533 | },
534 | {
535 | "name": "ball_on_plate_x",
536 | "type": {
537 | "category": "Number",
538 | "comment": "Ball X position in plate origin coordinates (m)"
539 | }
540 | },
541 | {
542 | "name": "ball_on_plate_y",
543 | "type": {
544 | "category": "Number",
545 | "comment": "Ball Y position in plate origin coordinates (m)"
546 | }
547 | },
548 | {
549 | "name": "ball_vel_x",
550 | "type": {
551 | "category": "Number",
552 | "comment": "Current ball X velocity ground truth (m/s)"
553 | }
554 | },
555 | {
556 | "name": "ball_vel_y",
557 | "type": {
558 | "category": "Number",
559 | "comment": "Current ball Y velocity ground truth (m/s)"
560 | }
561 | },
562 | {
563 | "name": "ball_vel_z",
564 | "type": {
565 | "category": "Number",
566 | "comment": "Current ball Z velocity ground truth (m/s)"
567 | }
568 | },
569 | {
570 | "name": "ball_qat_x",
571 | "type": {
572 | "category": "Number",
573 | "comment": "Current ball rotation quaternion X component"
574 | }
575 | },
576 | {
577 | "name": "ball_qat_y",
578 | "type": {
579 | "category": "Number",
580 | "comment": "Current ball rotation quaternion Y component"
581 | }
582 | },
583 | {
584 | "name": "ball_qat_z",
585 | "type": {
586 | "category": "Number",
587 | "comment": "Current ball rotation quaternion Z component"
588 | }
589 | },
590 | {
591 | "name": "ball_qat_w",
592 | "type": {
593 | "category": "Number",
594 | "comment": "Current ball rotation quaternion W component"
595 | }
596 | },
597 | {
598 | "name": "obstacle_distance",
599 | "type": {
600 | "category": "Number",
601 | "comment": "Scalar distance between ball and obstacle (m)"
602 | }
603 | },
604 | {
605 | "name": "obstacle_direction",
606 | "type": {
607 | "category": "Number",
608 | "comment": "Direction to obstacle (rad)"
609 | }
610 | },
611 | {
612 | "name": "estimated_x",
613 | "type": {
614 | "category": "Number",
615 | "start": -{{plate_radius}},
616 | "stop": {{plate_radius}},
617 | "comment": "Estimated ball X position, with ball noise applied (m)"
618 | }
619 | },
620 | {
621 | "name": "estimated_y",
622 | "type": {
623 | "category": "Number",
624 | "start": -{{plate_radius}},
625 | "stop": {{plate_radius}},
626 | "comment": "Estimated ball Y position, with ball noise applied (m)"
627 | }
628 | },
629 | {
630 | "name": "estimated_radius",
631 | "type": {
632 | "category": "Number",
633 | "start": -{{plate_radius}},
634 | "stop": {{plate_radius}},
635 | "comment": "Estimated ball radius, with ball noise applied (m)"
636 | }
637 | },
638 | {
639 | "name": "estimated_vel_x",
640 | "type": {
641 | "category": "Number",
642 | "start": -1.0,
643 | "stop": 1.0,
644 | "comment": "Estimated ball X velocity, with ball noise applied (m)"
645 | }
646 | },
647 | {
648 | "name": "estimated_vel_y",
649 | "type": {
650 | "category": "Number",
651 | "start": -1.0,
652 | "stop": 1.0,
653 | "comment": "Estimated ball Y velocity, with ball noise applied (m)"
654 | }
655 | },
656 | {
657 | "name": "estimated_speed",
658 | "type": {
659 | "category": "Number",
660 | "start": -1.0,
661 | "stop": 1.0,
662 | "comment": "Current estimated ball speed towards target (m/s)"
663 | }
664 | },
665 | {
666 | "name": "estimated_direction",
667 | "type": {
668 | "category": "Number",
669 | "start": -4,
670 | "stop": 4,
671 | "comment": "Current estimated ball heading towards target in X,Y plane (rad)"
672 | }
673 | },
674 | {
675 | "name": "estimated_distance",
676 | "type": {
677 | "category": "Number",
678 | "start": 0,
679 | "stop": 1.0,
680 | "comment": "Current distance between ball and target in X,Y plane (m)"
681 | }
682 | },
683 | {
684 | "name": "ball_noise",
685 | "type": {
686 | "category": "Number",
687 | "comment": "Current ball noise magnitude (m)"
688 | }
689 | },
690 | {
691 | "name": "plate_noise",
692 | "type": {
693 | "category": "Number",
694 | "comment": "Current plate angle noise magnitude (rad)"
695 | }
696 | },
697 | {
698 | "name": "ball_fell_off",
699 | "type": {
700 | "category": "Number",
701 | "namedValues": [
702 | {
703 | "name": "False",
704 | "value": 0
705 | },
706 | {
707 | "name": "True",
708 | "value": 1
709 | }
710 | ],
711 | "comment": "Ball has fallen off the plate and is unreachable [0,1]"
712 | }
713 | },
714 | {
715 | "name": "iteration_count",
716 | "type": {
717 | "category": "Number",
718 | "comment": "Current iteration count in this episode"
719 | }
720 | }
721 | ]
722 | }
723 | }
724 | }
725 |
--------------------------------------------------------------------------------
/moab_model.py:
--------------------------------------------------------------------------------
1 | """
2 | Simulator for the Moab plate+ball balancing device.
3 | """
4 | __author__ = "Mike Estee"
5 | __copyright__ = "Copyright 2021, Microsoft Corp."
6 |
7 | # pyright: strict
8 |
9 | import math
10 | import random
11 | from typing import Dict, Tuple, cast
12 |
13 | import numpy as np
14 | from pyrr import Quaternion, Vector3, matrix44, quaternion, ray, vector
15 | from pyrr.geometric_tests import ray_intersect_plane
16 | from pyrr.plane import create_from_position
17 |
18 | # Some type aliases for clarity
19 | Plane = np.ndarray
20 | Ray = np.ndarray
21 |
22 | DEFAULT_TIME_DELTA = 0.045 # s, 45ms
23 | DEFAULT_GRAVITY = 9.81 # m/s^2, Earth: there's no place like it.
24 |
25 | DEFAULT_BALL_RADIUS = 0.02 # m, Ping-Pong ball: 20mm
26 | DEFAULT_BALL_SHELL = 0.0002 # m, Ping-Pong ball: 0.2mm
27 | DEFAULT_BALL_MASS = 0.0027 # kg, Ping-Pong ball: 2.7g
28 |
29 | DEFAULT_OBSTACLE_RADIUS = 0.0 # m, if radius is zero, obstacle is disabled
30 | DEFAULT_OBSTACLE_X = 0.03 # m, arbitrarily chosen
31 | DEFAULT_OBSTACLE_Y = 0.03 # m, arbitrarily chosen
32 |
33 | DEFAULT_PLATE_RADIUS = 0.225 / 2.0 # m, Moab: 225mm dia
34 | PLATE_ORIGIN_TO_SURFACE_OFFSET = (
35 | 0.009 # 9mm offset from plate rot origin to plate surface
36 | )
37 |
38 | # plate limits
39 | PLATE_HEIGHT_MAX = 0.040 # m, Moab: 40mm
40 | DEFAULT_PLATE_HEIGHT = PLATE_HEIGHT_MAX / 2.0
41 | DEFAULT_PLATE_ANGLE_LIMIT = math.radians(44.0 * 0.5) # rad, 1/2 full range
42 | DEFAULT_PLATE_Z_LIMIT = PLATE_HEIGHT_MAX / 2.0 # m, +/- limit from center Z pos
43 |
44 | # default ball Z position
45 | DEFAULT_BALL_Z_POSITION = (
46 | DEFAULT_PLATE_HEIGHT + PLATE_ORIGIN_TO_SURFACE_OFFSET + DEFAULT_BALL_RADIUS
47 | )
48 |
49 | PLATE_MAX_Z_VELOCITY = 1.0 # m/s
50 | PLATE_Z_ACCEL = 10.0 # m/s^2
51 |
52 | # Moab measured velocity at 15deg in 3/60ths, or 300deg/s
53 | DEFAULT_PLATE_MAX_ANGULAR_VELOCITY = (60.0 / 3.0) * math.radians(15) # rad/s
54 |
55 | # Set acceleration to get the plate up to velocity in 1/100th of a sec
56 | DEFAULT_PLATE_ANGULAR_ACCEL = (
57 | 100.0 / 1.0
58 | ) * DEFAULT_PLATE_MAX_ANGULAR_VELOCITY # rad/s^2
59 |
60 | # useful constants
61 | X_AXIS = np.array([1.0, 0.0, 0.0])
62 | Y_AXIS = np.array([0.0, 1.0, 0.0])
63 | Z_AXIS = np.array([0.0, 0.0, 1.0])
64 |
65 | # Sensor Actuator Noises
66 | DEFAULT_PLATE_NOISE = 0.0 # noise added to plate_theta_* (rad)
67 | DEFAULT_BALL_NOISE = 0.0 # noise added to estimated_* ball location (m)
68 | DEFAULT_JITTER = 0.0 # jitter added to step_time (s)
69 |
70 |
71 | def clamp(val: float, min_val: float, max_val: float):
72 | return min(max_val, max(min_val, val))
73 |
74 |
75 | class MoabModel:
76 | def __init__(self):
77 | self.reset()
78 |
79 | def reset(self):
80 | """
81 | Resets the model to known default state.
82 |
83 | If further changes are applied after reseting, the caller should call:
84 | model.update_plate(True)
85 | model.update_ball(True)
86 | """
87 | # general config
88 | self.time_delta = DEFAULT_TIME_DELTA
89 | self.jitter = DEFAULT_JITTER
90 | self.step_time = self.time_delta
91 | self.elapsed_time = 0.0
92 | self.gravity = DEFAULT_GRAVITY
93 |
94 | # plate config
95 | self.plate_noise = DEFAULT_PLATE_NOISE
96 | self.plate_radius = DEFAULT_PLATE_RADIUS
97 | self.plate_theta_limit = DEFAULT_PLATE_ANGLE_LIMIT
98 | self.plate_theta_vel_limit = DEFAULT_PLATE_MAX_ANGULAR_VELOCITY
99 | self.plate_theta_acc = DEFAULT_PLATE_ANGULAR_ACCEL
100 | self.plate_z_limit = DEFAULT_PLATE_Z_LIMIT
101 |
102 | # ball config
103 | self.ball_noise = DEFAULT_BALL_NOISE
104 | self.ball_mass = DEFAULT_BALL_MASS
105 | self.ball_radius = DEFAULT_BALL_RADIUS
106 | self.ball_shell = DEFAULT_BALL_SHELL
107 |
108 | # control input (unitless) [-1..1]
109 | self.pitch = 0.0
110 | self.roll = 0.0
111 | self.height_z = 0.0
112 |
113 | # plate state
114 | self.plate_theta_x = 0.0
115 | self.plate_theta_y = 0.0
116 | self.plate = Vector3([0.0, 0.0, DEFAULT_PLATE_HEIGHT])
117 |
118 | self.plate_theta_vel_x = 0.0
119 | self.plate_theta_vel_y = 0.0
120 | self.plate_vel_z = 0.0
121 |
122 | # ball state
123 | self.ball = Vector3([0.0, 0.0, DEFAULT_BALL_Z_POSITION])
124 | self.ball_vel = Vector3([0.0, 0.0, 0.0])
125 | self.ball_qat = Quaternion([0.0, 0.0, 0.0, 1.0])
126 | self.ball_on_plate = Vector3(
127 | [0.0, 0.0, PLATE_ORIGIN_TO_SURFACE_OFFSET + DEFAULT_BALL_RADIUS]
128 | )
129 |
130 | # current target
131 | self.target_x = 0.0
132 | self.target_y = 0.0
133 |
134 | # current obstacle
135 | self.obstacle_distance = 0.0
136 | self.obstacle_direction = 0.0
137 | self.obstacle_radius = 0.0
138 | self.obstacle_x = 0.0
139 | self.obstacle_y = 0.0
140 |
141 | # camera observed estimated metrics
142 | self.estimated_x = 0.0
143 | self.estimated_y = 0.0
144 | self.estimated_vel_x = 0.0
145 | self.estimated_vel_y = 0.0
146 | self.estimated_radius = self.ball_radius
147 |
148 | # target relative polar coords/vel
149 | self.estimated_speed = 0.0
150 | self.estimated_direction = 0.0
151 | self.estimated_distance = 0.0
152 |
153 | self.prev_estimated_x = 0.0
154 | self.prev_estimated_y = 0.0
155 |
156 | # meta
157 | self.iteration_count = 0
158 |
159 | # now that the base state has been set, run an update
160 | # to make sure the all variables are internally constistent
161 | self.update_plate(True)
162 | self.update_ball(True)
163 |
164 | def halted(self) -> bool:
165 | """
166 | Returns True if the ball is off the plate.
167 | """
168 | # ball.z relative to plate
169 | zpos = self.ball.z - (
170 | self.plate.z + self.ball_radius + PLATE_ORIGIN_TO_SURFACE_OFFSET
171 | )
172 |
173 | # ball distance from ball position on plate at origin
174 | distance_to_center = math.sqrt(
175 | math.pow(self.ball.x, 2.0)
176 | + math.pow(self.ball.y, 2.0)
177 | + math.pow(zpos, 2.0)
178 | )
179 |
180 | return distance_to_center > self.plate_radius
181 |
182 | def step(self):
183 | """
184 | Single step the simulation.
185 |
186 | The current actions will be applied, and the model evaluated.
187 | All state variables will be updated.
188 | """
189 | self.step_time = self.time_delta + MoabModel.random_noise(self.jitter)
190 | self.elapsed_time += self.step_time
191 |
192 | self.update_plate(False)
193 | self.update_ball(False)
194 |
195 | # update meta
196 | self.iteration_count += 1
197 |
198 | # returns a noise value in the range [-scalar .. scalar] with a gaussian distribution
199 | @staticmethod
200 | def random_noise(scalar: float) -> float:
201 | return scalar * clamp(
202 | random.gauss(mu=0, sigma=0.333), -1, 1
203 | ) # mean zero gauss with sigma = ~sqrt(scalar)/3
204 |
205 | @staticmethod
206 | def accel_param(
207 | q: float, dest: float, vel: float, acc: float, max_vel: float, delta_t: float
208 | ) -> Tuple[float, float]:
209 | """
210 | perform a linear acceleration of variable towards a destination
211 | with a hard stop at the destination. returns the position and velocity
212 | after delta_t has elapsed.
213 |
214 | q: initial position
215 | dest: target destination
216 | vel: current velocity
217 | acc: acceleration constant
218 | max_vel: maximum velocity
219 | delta_t: time delta
220 |
221 | returns: (final_position, final_velocity)
222 | """
223 | # direction of accel
224 | dir = 0.0
225 | if q < dest:
226 | dir = 1.0
227 | if q > dest:
228 | dir = -1.0
229 |
230 | # calculate the change in velocity and position
231 | acc = acc * dir * delta_t
232 | vel_end = clamp(vel + acc * delta_t, -max_vel, max_vel)
233 | vel_avg = (vel + vel_end) * 0.5
234 | delta = vel_avg * delta_t
235 | vel = vel_end
236 |
237 | # moving towards the dest?
238 | if (dir > 0 and q < dest and q + delta < dest) or (
239 | dir < 0 and q > dest and q + delta > dest
240 | ):
241 | q = q + delta
242 |
243 | # stop at dest
244 | else:
245 | q = dest
246 | vel = 0
247 |
248 | return (q, vel)
249 |
250 | @staticmethod
251 | def heading_to_point(
252 | start_x: float,
253 | start_y: float,
254 | vel_x: float,
255 | vel_y: float,
256 | point_x: float,
257 | point_y: float,
258 | ):
259 | """
260 | Return a heading, in 2D RH coordinate system.
261 | x,y: the current position of the object
262 | vel_x, vel_y: the current velocity vector of motion for the object
263 | point_x, point_y: the destination point to head towards
264 |
265 | returns: offset angle in radians in the range [-pi .. pi]
266 | where:
267 | 0.0: object is moving directly towards the point
268 | [-pi .. <0]: object is moving to the "right" of the point
269 | [>0 .. -pi]: object is moving to the "left" of the point
270 | [-pi, pi]: object is moving directly away from the point
271 | """
272 | # vector to point
273 | dx = point_x - start_x
274 | dy = point_y - start_y
275 |
276 | # if the ball is already at the target location or
277 | # is not moving, return a heading of 0 so we don't
278 | # attempt to normalize a zero-length vector
279 | if dx == 0 and dy == 0:
280 | return 0
281 | if vel_x == 0 and vel_y == 0:
282 | return 0
283 |
284 | # vectors and lengths
285 | u = vector.normalize([dx, dy, 0.0])
286 | v = vector.normalize([vel_x, vel_y, 0.0])
287 | ul = vector.length(u)
288 | vl = vector.length(v)
289 |
290 | # no velocity? already on the target?
291 | angle = 0.0
292 | if (ul != 0.0) and (vl != 0.0):
293 | # angle between vectors
294 | uv_dot = vector.dot(u, v)
295 |
296 | # signed angle
297 | x = u[0]
298 | y = u[1]
299 | angle = math.atan2(vector.dot([-y, x, 0.0], v), uv_dot)
300 | if math.isnan(angle):
301 | angle = 0.0
302 | return angle
303 |
304 | @staticmethod
305 | def distance_to_point(x: float, y: float, point_x: float, point_y: float) -> float:
306 | """
307 | Return the distance between two 2D points.
308 | """
309 | dx = point_x - x
310 | dy = point_y - y
311 | return math.sqrt((dx ** 2.0) + (dy ** 2.0))
312 |
313 | # convert X/Y theta components into a Z-Up RH plane normal
314 | def _plate_nor(self) -> Vector3:
315 | x_rot = matrix44.create_from_axis_rotation(
316 | axis=X_AXIS, theta=self.plate_theta_x
317 | )
318 | y_rot = matrix44.create_from_axis_rotation(
319 | axis=Y_AXIS, theta=self.plate_theta_y
320 | )
321 |
322 | # pitch then roll
323 | nor = matrix44.apply_to_vector(mat=x_rot, vec=Z_AXIS)
324 | nor = matrix44.apply_to_vector(mat=y_rot, vec=nor)
325 | nor = vector.normalize(nor)
326 |
327 | return Vector3(nor)
328 |
329 | def update_plate(self, plate_reset: bool = False):
330 | # Find the target xth,yth & zpos
331 | # convert xy[-1..1] to zx[-self.plate_theta_limit .. self.plate_theta_limit]
332 | # convert z[-1..1] to [PLATE_HEIGHT_MAX/2 - self.plate_z_limit .. PLATE_HEIGHT_MAX/2 + self.plate_z_limit]
333 | theta_x_target = self.plate_theta_limit * self.pitch # pitch around X axis
334 | theta_y_target = self.plate_theta_limit * self.roll # roll around Y axis
335 | z_target = (self.height_z * self.plate_z_limit) + PLATE_HEIGHT_MAX / 2.0
336 |
337 | # quantize target positions to whole degree increments
338 | # the Moab hardware can only command by whole degrees
339 | theta_y_target = math.radians(round(math.degrees(theta_y_target)))
340 | theta_x_target = math.radians(round(math.degrees(theta_x_target)))
341 |
342 | # get the current xth,yth & zpos
343 | theta_x, theta_y = self.plate_theta_x, self.plate_theta_y
344 | z_pos = self.plate.z
345 |
346 | # on reset, bypass the motion equations
347 | if plate_reset:
348 | theta_x = theta_x_target
349 | theta_y = theta_y_target
350 | z_pos = z_target
351 |
352 | # smooth transition to target based on accel and velocity limits
353 | else:
354 | theta_x, self.plate_theta_vel_x = MoabModel.accel_param(
355 | theta_x,
356 | theta_x_target,
357 | self.plate_theta_vel_x,
358 | self.plate_theta_acc,
359 | self.plate_theta_vel_limit,
360 | self.step_time,
361 | )
362 | theta_y, self.plate_theta_vel_y = MoabModel.accel_param(
363 | theta_y,
364 | theta_y_target,
365 | self.plate_theta_vel_y,
366 | self.plate_theta_acc,
367 | self.plate_theta_vel_limit,
368 | self.step_time,
369 | )
370 | z_pos, self.plate_vel_z = MoabModel.accel_param(
371 | z_pos,
372 | z_target,
373 | self.plate_vel_z,
374 | PLATE_Z_ACCEL,
375 | PLATE_MAX_Z_VELOCITY,
376 | self.step_time,
377 | )
378 |
379 | # add noise to the plate positions
380 | theta_x += MoabModel.random_noise(self.plate_noise)
381 | theta_y += MoabModel.random_noise(self.plate_noise)
382 |
383 | # clamp to range limits
384 | theta_x = clamp(theta_x, -self.plate_theta_limit, self.plate_theta_limit)
385 | theta_y = clamp(theta_y, -self.plate_theta_limit, self.plate_theta_limit)
386 | z_pos = clamp(
387 | z_pos,
388 | PLATE_HEIGHT_MAX / 2.0 - self.plate_z_limit,
389 | PLATE_HEIGHT_MAX / 2.0 + self.plate_z_limit,
390 | )
391 |
392 | # Now convert back to plane parameters
393 | self.plate_theta_x = theta_x
394 | self.plate_theta_y = theta_y
395 | self.plate.z = z_pos
396 |
397 | # ball intertia with radius and hollow radius
398 | # I = 2/5 * m * ((r^5 - h^5) / (r^3 - h^3))
399 | def _ball_inertia(self):
400 | hollow_radius = self.ball_radius - self.ball_shell
401 | return (
402 | 2.0
403 | / 5.0
404 | * self.ball_mass
405 | * (
406 | (math.pow(self.ball_radius, 5.0) - math.pow(hollow_radius, 5.0))
407 | / (math.pow(self.ball_radius, 3.0) - math.pow(hollow_radius, 3.0))
408 | )
409 | )
410 |
411 | def _camera_pos(self) -> Vector3:
412 | """ camera origin (lens center) in world space """
413 | return Vector3([0.0, 0.0, -0.052])
414 |
415 | def _update_estimated_ball(self, ball: Vector3):
416 | """
417 | Ray trace the ball position and an edge of the ball back to the camera
418 | origin and use the collision points with the tilted plate to estimate
419 | what a camera might perceive the ball position and size to be.
420 | """
421 | # contact ray from camera to plate
422 | camera = self._camera_pos()
423 | displacement = camera - self.ball
424 | displacement_radius = camera - (self.ball + Vector3([self.ball_radius, 0, 0]))
425 |
426 | ball_ray = ray.create(camera, displacement)
427 | ball_radius_ray = ray.create(camera, displacement_radius)
428 |
429 | surface_plane = self._surface_plane()
430 |
431 | contact = Vector3(ray_intersect_plane(ball_ray, surface_plane, False))
432 | radius_contact = Vector3(
433 | ray_intersect_plane(ball_radius_ray, surface_plane, False)
434 | )
435 |
436 | x, y = contact.x, contact.y
437 | r = math.fabs(contact.x - radius_contact.x)
438 |
439 | # add the noise in
440 | self.estimated_x = x + MoabModel.random_noise(self.ball_noise)
441 | self.estimated_y = y + MoabModel.random_noise(self.ball_noise)
442 | self.estimated_radius = r + MoabModel.random_noise(self.ball_noise)
443 |
444 | # Use n-1 states to calculate an estimated velocity.
445 | self.estimated_vel_x = (
446 | self.estimated_x - self.prev_estimated_x
447 | ) / self.step_time
448 | self.estimated_vel_y = (
449 | self.estimated_y - self.prev_estimated_y
450 | ) / self.step_time
451 |
452 | # distance to target
453 | self.estimated_distance = MoabModel.distance_to_point(
454 | self.estimated_x, self.estimated_y, self.target_x, self.target_y
455 | )
456 |
457 | # update the derived states
458 | self.estimated_speed = cast(
459 | float, vector.length([self.ball_vel.x, self.ball_vel.y, self.ball_vel.z])
460 | )
461 |
462 | self.estimated_direction = MoabModel.heading_to_point(
463 | self.estimated_x,
464 | self.estimated_y,
465 | self.estimated_vel_x,
466 | self.estimated_vel_y,
467 | self.target_x,
468 | self.target_y,
469 | )
470 |
471 | # update for next time
472 | self.prev_estimated_x = self.estimated_x
473 | self.prev_estimated_y = self.estimated_y
474 |
475 | # update ball position in plate origin coordinates, and obstacle distance and direction
476 | self.ball_on_plate = self.world_to_plate(self.ball.x, self.ball.y, self.ball.z)
477 | self.obstacle_distance = self._get_obstacle_distance()
478 | self.obstacle_direction = MoabModel.heading_to_point(
479 | self.ball.x,
480 | self.ball.y,
481 | self.ball_vel.x,
482 | self.ball_vel.y,
483 | self.obstacle_x,
484 | self.obstacle_y,
485 | )
486 |
487 | def _get_obstacle_distance(self) -> float:
488 | # Ignore z value, calculate distance between obstacle and ball projection on plate
489 | distance_between_centers = math.sqrt(
490 | math.pow(self.ball_on_plate.x - self.obstacle_x, 2.0)
491 | + math.pow(self.ball_on_plate.y - self.obstacle_y, 2.0)
492 | )
493 |
494 | # Negative distance to obstacle means the ball and obstacle are overlapping
495 | return distance_between_centers - self.ball_radius - self.obstacle_radius
496 |
497 | def _surface_plane(self) -> Plane:
498 | """
499 | Return the surface plane of the plate
500 | """
501 | plate_surface = np.array(
502 | [self.plate.x, self.plate.y, self.plate.z + PLATE_ORIGIN_TO_SURFACE_OFFSET]
503 | )
504 | return create_from_position(plate_surface, self._plate_nor())
505 |
506 | def _motion_for_time(
507 | self, u: Vector3, a: Vector3, t: float
508 | ) -> Tuple[Vector3, Vector3]:
509 | """
510 | Equations of motion for displacement and final velocity
511 | u: initial velocity
512 | a: acceleration
513 | d: displacement
514 | v: final velocity
515 |
516 | d = ut + 1/2at^2
517 | v = u + at
518 |
519 | returns (d, v)
520 | """
521 | d = (u * t) + (0.5 * a * (t ** 2))
522 | v = u + a * t
523 | return d, v
524 |
525 | def _update_ball_z(self):
526 | self.ball.z = (
527 | self.ball.x * math.sin(-self.plate_theta_y)
528 | + self.ball.y * math.sin(self.plate_theta_x)
529 | + self.ball_radius
530 | + self.plate.z
531 | + PLATE_ORIGIN_TO_SURFACE_OFFSET
532 | )
533 |
534 | def _ball_plate_contact(self, step_t: float) -> float:
535 | # NOTE: the x_theta axis creates motion in the Y-axis, and vice versa
536 | # x_theta, y_theta = self._xy_theta_from_nor(self.plate_nor.xyz)
537 | x_theta = self.plate_theta_x
538 | y_theta = self.plate_theta_y
539 |
540 | # Equations for acceleration on a plate at rest
541 | # accel = (mass * g * theta) / (mass + inertia / radius^2)
542 | # (y_theta,x are intentional swapped here.)
543 | theta = Vector3([y_theta, -x_theta, 0])
544 | self.ball_acc = (
545 | theta
546 | / (self.ball_mass + self._ball_inertia() / (self.ball_radius ** 2))
547 | * self.ball_mass
548 | * self.gravity
549 | )
550 |
551 | # get contact displacement
552 | disp, vel = self._motion_for_time(self.ball_vel, self.ball_acc, step_t)
553 |
554 | # simplified ball mechanics against a plane
555 | self.ball.x += disp.x
556 | self.ball.y += disp.y
557 | self._update_ball_z()
558 | self.ball_vel = vel
559 |
560 | # For rotation on plate motion we use infinite friction and
561 | # perfect ball / plate coupling.
562 | # Calculate the distance we traveled across the plate during
563 | # this time slice.
564 | rot_distance = math.hypot(disp.x, disp.y)
565 | if rot_distance > 0:
566 | # Calculate the fraction of the circumference that we traveled
567 | # (in radians).
568 | rot_angle = rot_distance / self.ball_radius
569 |
570 | # Create a quaternion that represents the delta rotation for this time period.
571 | # Note that we translate the (x, y) direction into (y, -x) because we're
572 | # creating a vector that represents the axis of rotation which is normal
573 | # to the direction the ball traveled in the x/y plane.
574 | rot_q = quaternion.normalize(
575 | np.array(
576 | [
577 | disp.y / rot_distance * math.sin(rot_angle / 2.0),
578 | -disp.x / rot_distance * math.sin(rot_angle / 2.0),
579 | 0.0,
580 | math.cos(rot_angle / 2.0),
581 | ]
582 | )
583 | )
584 |
585 | old_rot = self.ball_qat.xyzw
586 | new_rot = quaternion.cross(quat1=old_rot, quat2=rot_q)
587 | self.ball_qat.xyzw = quaternion.normalize(new_rot)
588 | return 0.0
589 |
590 | def plate_to_world(self, x: float, y: float, z: float) -> Vector3:
591 | # rotate
592 | x_rot = matrix44.create_from_axis_rotation([1.0, 0.0, 0.0], self.plate_theta_x)
593 | y_rot = matrix44.create_from_axis_rotation([0.0, 1.0, 0.0], self.plate_theta_y)
594 | vec = matrix44.apply_to_vector(mat=x_rot, vec=[x, y, z])
595 | vec = matrix44.apply_to_vector(mat=y_rot, vec=vec)
596 |
597 | # translate
598 | move = matrix44.create_from_translation(
599 | [self.plate.x, self.plate.y, self.plate.z + PLATE_ORIGIN_TO_SURFACE_OFFSET]
600 | )
601 | vec = matrix44.apply_to_vector(mat=move, vec=vec)
602 |
603 | return Vector3(vec)
604 |
605 | def world_to_plate(self, x: float, y: float, z: float) -> Vector3:
606 | move = matrix44.create_from_translation(
607 | [
608 | -self.plate.x,
609 | -self.plate.y,
610 | -(self.plate.z + PLATE_ORIGIN_TO_SURFACE_OFFSET),
611 | ]
612 | )
613 | vec = matrix44.apply_to_vector(mat=move, vec=[x, y, z])
614 |
615 | # rotate
616 | x_rot = matrix44.create_from_axis_rotation([1.0, 0.0, 0.0], -self.plate_theta_x)
617 | y_rot = matrix44.create_from_axis_rotation([0.0, 1.0, 0.0], -self.plate_theta_y)
618 | vec = matrix44.apply_to_vector(mat=x_rot, vec=vec)
619 | vec = matrix44.apply_to_vector(mat=y_rot, vec=vec)
620 |
621 | return Vector3(vec)
622 |
623 | def set_initial_ball(self, x: float, y: float, z: float):
624 | self.ball.xyz = [x, y, z]
625 | self._update_ball_z()
626 |
627 | # Set initial observations
628 | self._update_estimated_ball(self.ball)
629 | pass
630 |
631 | def update_ball(self, ball_reset: bool = False):
632 | """
633 | Update the ball position with the physics model.
634 | """
635 | if ball_reset:
636 | # this just ensures that the ball is on the plate
637 | self._update_ball_z()
638 | else:
639 | self._ball_plate_contact(self.step_time)
640 |
641 | # Finally, lets make some approximations for observations
642 | self._update_estimated_ball(self.ball)
643 |
644 | def state(self) -> Dict[str, float]:
645 | # x_theta, y_theta = self._xy_theta_from_nor(self.plate_nor)
646 | plate_nor = self._plate_nor()
647 |
648 | return dict(
649 | # reflected input controls
650 | roll=self.roll,
651 | pitch=self.pitch,
652 | height_z=self.height_z,
653 | # reflected constants
654 | time_delta=self.time_delta,
655 | jitter=self.jitter,
656 | step_time=self.step_time,
657 | elapsed_time=self.elapsed_time,
658 | gravity=self.gravity,
659 | plate_radius=self.plate_radius,
660 | plate_theta_vel_limit=self.plate_theta_vel_limit,
661 | plate_theta_acc=self.plate_theta_acc,
662 | plate_theta_limit=self.plate_theta_limit,
663 | plate_z_limit=self.plate_z_limit,
664 | ball_mass=self.ball_mass,
665 | ball_radius=self.ball_radius,
666 | ball_shell=self.ball_shell,
667 | obstacle_radius=self.obstacle_radius,
668 | obstacle_x=self.obstacle_x,
669 | obstacle_y=self.obstacle_y,
670 | target_x=self.target_x,
671 | target_y=self.target_y,
672 | # modelled plate metrics
673 | plate_x=self.plate.x,
674 | plate_y=self.plate.y,
675 | plate_z=self.plate.z,
676 | plate_nor_x=plate_nor.x,
677 | plate_nor_y=plate_nor.y,
678 | plate_nor_z=plate_nor.z,
679 | plate_theta_x=self.plate_theta_x,
680 | plate_theta_y=self.plate_theta_y,
681 | plate_theta_vel_x=self.plate_theta_vel_x,
682 | plate_theta_vel_y=self.plate_theta_vel_y,
683 | plate_vel_z=self.plate_vel_z,
684 | # modelled ball metrics
685 | ball_x=self.ball.x,
686 | ball_y=self.ball.y,
687 | ball_z=self.ball.z,
688 | ball_vel_x=self.ball_vel.x,
689 | ball_vel_y=self.ball_vel.y,
690 | ball_vel_z=self.ball_vel.z,
691 | ball_qat_x=self.ball_qat.x,
692 | ball_qat_y=self.ball_qat.y,
693 | ball_qat_z=self.ball_qat.z,
694 | ball_qat_w=self.ball_qat.w,
695 | ball_on_plate_x=self.ball_on_plate.x,
696 | ball_on_plate_y=self.ball_on_plate.y,
697 | obstacle_distance=self.obstacle_distance,
698 | obstacle_direction=self.obstacle_direction,
699 | # modelled camera observations
700 | estimated_x=self.estimated_x,
701 | estimated_y=self.estimated_y,
702 | estimated_radius=self.estimated_radius,
703 | estimated_vel_x=self.estimated_vel_x,
704 | estimated_vel_y=self.estimated_vel_y,
705 | # modelled positions and velocities
706 | estimated_speed=self.estimated_speed,
707 | estimated_direction=self.estimated_direction,
708 | estimated_distance=self.estimated_distance,
709 | ball_noise=self.ball_noise,
710 | plate_noise=self.plate_noise,
711 | # meta vars
712 | ball_fell_off=1 if self.halted() else 0,
713 | iteration_count=self.iteration_count,
714 | )
715 |
--------------------------------------------------------------------------------
/moab_sim.py:
--------------------------------------------------------------------------------
1 | """
2 | Simulator for the Moab plate+ball balancing device.
3 | """
4 | __author__ = "Mike Estee"
5 | __copyright__ = "Copyright 2021, Microsoft Corp."
6 |
7 | # We need to disable a check because the typeshed stubs for jinja are incomplete.
8 | # pyright: strict, reportUnknownMemberType=false
9 |
10 | import logging
11 | import os
12 | import sys
13 | import json
14 |
15 | from jinja2 import Template
16 | from pyrr import matrix33, vector
17 |
18 | from moab_model import MoabModel, clamp
19 |
20 | from bonsai_common import SimulatorSession, Schema
21 | from microsoft_bonsai_api.simulator.generated.models import SimulatorInterface
22 | from microsoft_bonsai_api.simulator.client import BonsaiClientConfig
23 |
24 | log = logging.getLogger(__name__)
25 |
26 |
27 | class MoabSim(SimulatorSession):
28 | def __init__(self, config: BonsaiClientConfig):
29 | super().__init__(config)
30 | self.model = MoabModel()
31 | self._episode_count = 0
32 | self.model.reset()
33 |
34 | # callbacks
35 | def halted(self) -> bool:
36 | return self.model.halted()
37 |
38 | def get_interface(self) -> SimulatorInterface:
39 | interface_file_path = os.path.join(
40 | os.path.dirname(os.path.abspath(__file__)), "moab_interface.json"
41 | )
42 |
43 | # load the template
44 | try:
45 | with open(interface_file_path, "r") as file:
46 | template_str = file.read()
47 | except:
48 | log.info(
49 | "Failed to load interface template file: {}".format(interface_file_path)
50 | )
51 | raise
52 |
53 | # render the template with our constants
54 | template = Template(template_str)
55 | interface_str = template.render(
56 | initial_pitch=self.model.pitch,
57 | initial_roll=self.model.roll,
58 | initial_height_z=self.model.height_z,
59 | time_delta=self.model.time_delta,
60 | gravity=self.model.time_delta,
61 | plate_radius=self.model.plate_radius,
62 | plate_theta_vel_limit=self.model.plate_theta_vel_limit,
63 | plate_theta_acc=self.model.plate_theta_acc,
64 | plate_theta_limit=self.model.plate_theta_limit,
65 | plate_z_limit=self.model.plate_z_limit,
66 | ball_mass=self.model.ball_mass,
67 | ball_radius=self.model.ball_radius,
68 | ball_shell=self.model.ball_shell,
69 | obstacle_radius=self.model.obstacle_radius,
70 | obstacle_x=self.model.obstacle_x,
71 | obstacle_y=self.model.obstacle_y,
72 | target_x=self.model.target_x,
73 | target_y=self.model.target_y,
74 | initial_x=self.model.ball.x,
75 | initial_y=self.model.ball.y,
76 | initial_z=self.model.ball.z,
77 | initial_vel_x=self.model.ball_vel.x,
78 | initial_vel_y=self.model.ball_vel.y,
79 | initial_vel_z=self.model.ball_vel.z,
80 | initial_speed=0,
81 | initial_direction=0,
82 | ball_noise=self.model.ball_noise,
83 | plate_noise=self.model.plate_noise,
84 | )
85 | interface = json.loads(interface_str)
86 | return SimulatorInterface(
87 | name=interface["name"],
88 | timeout=interface["timeout"],
89 | simulator_context=self.get_simulator_context(),
90 | description=interface["description"],
91 | )
92 |
93 | def get_state(self) -> Schema:
94 | return self.model.state()
95 |
96 | def _set_velocity_for_speed_and_direction(self, speed: float, direction: float):
97 | # get the heading
98 | dx = self.model.target_x - self.model.ball.x
99 | dy = self.model.target_y - self.model.ball.y
100 |
101 | # direction is meaningless if we're already at the target
102 | if (dx != 0) or (dy != 0):
103 |
104 | # set the magnitude
105 | vel = vector.set_length([dx, dy, 0.0], speed)
106 |
107 | # rotate by direction around Z-axis at ball position
108 | rot = matrix33.create_from_axis_rotation([0.0, 0.0, 1.0], direction)
109 | vel = matrix33.apply_to_vector(rot, vel)
110 |
111 | # unpack into ball velocity
112 | self.model.ball_vel.x = vel[0]
113 | self.model.ball_vel.y = vel[1]
114 | self.model.ball_vel.z = vel[2]
115 |
116 | def episode_start(self, config: Schema) -> None:
117 | # return to known good state to avoid accidental episode-episode dependencies
118 | self.model.reset()
119 |
120 | # initial control state. these are all [-1..1] unitless
121 | self.model.roll = config.get("initial_roll", self.model.roll)
122 | self.model.pitch = config.get("initial_pitch", self.model.pitch)
123 |
124 | self.model.height_z = config.get("initial_height_z", self.model.height_z)
125 |
126 | # constants, SI units.
127 | self.model.time_delta = config.get("time_delta", self.model.time_delta)
128 | self.model.jitter = config.get("jitter", self.model.jitter)
129 | self.model.gravity = config.get("gravity", self.model.gravity)
130 | self.model.plate_theta_vel_limit = config.get(
131 | "plate_theta_vel_limit", self.model.plate_theta_vel_limit
132 | )
133 | self.model.plate_theta_acc = config.get(
134 | "plate_theta_acc", self.model.plate_theta_acc
135 | )
136 | self.model.plate_theta_limit = config.get(
137 | "plate_theta_limit", self.model.plate_theta_limit
138 | )
139 | self.model.plate_z_limit = config.get("plate_z_limit", self.model.plate_z_limit)
140 |
141 | self.model.ball_mass = config.get("ball_mass", self.model.ball_mass)
142 | self.model.ball_radius = config.get("ball_radius", self.model.ball_radius)
143 | self.model.ball_shell = config.get("ball_shell", self.model.ball_shell)
144 |
145 | self.model.obstacle_radius = config.get(
146 | "obstacle_radius", self.model.obstacle_radius
147 | )
148 | self.model.obstacle_x = config.get("obstacle_x", self.model.obstacle_x)
149 | self.model.obstacle_y = config.get("obstacle_y", self.model.obstacle_y)
150 |
151 | # a target position the AI can try and move the ball to
152 | self.model.target_x = config.get("target_x", self.model.target_x)
153 | self.model.target_y = config.get("target_y", self.model.target_y)
154 |
155 | # observation config
156 | self.model.ball_noise = config.get("ball_noise", self.model.ball_noise)
157 | self.model.plate_noise = config.get("plate_noise", self.model.plate_noise)
158 |
159 | # now we can update the initial plate metrics from the constants and the controls
160 | self.model.update_plate(plate_reset=True)
161 |
162 | # initial ball state after updating plate
163 | self.model.set_initial_ball(
164 | config.get("initial_x", self.model.ball.x),
165 | config.get("initial_y", self.model.ball.y),
166 | config.get("initial_z", self.model.ball.z),
167 | )
168 |
169 | # velocity set as a vector
170 | self.model.ball_vel.x = config.get("initial_vel_x", self.model.ball_vel.x)
171 | self.model.ball_vel.y = config.get("initial_vel_y", self.model.ball_vel.y)
172 | self.model.ball_vel.z = config.get("initial_vel_z", self.model.ball_vel.z)
173 |
174 | # velocity set as a speed/direction towards target
175 | initial_speed = config.get("initial_speed", None)
176 | initial_direction = config.get("initial_direction", None)
177 | if initial_speed is not None and initial_direction is not None:
178 | self._set_velocity_for_speed_and_direction(initial_speed, initial_direction)
179 |
180 | # new episode, iteration count reset
181 | self.iteration_count = 0
182 | self._episode_count += 1
183 |
184 | def episode_step(self, action: Schema):
185 | # use new syntax or fall back to old parameter names
186 | self.model.roll = action.get("input_roll", self.model.roll)
187 | self.model.pitch = action.get("input_pitch", self.model.pitch)
188 |
189 | # clamp inputs to legal ranges
190 | self.model.roll = clamp(self.model.roll, -1.0, 1.0)
191 | self.model.pitch = clamp(self.model.pitch, -1.0, 1.0)
192 |
193 | self.model.height_z = clamp(
194 | action.get("input_height_z", self.model.height_z), -1.0, 1.0
195 | )
196 |
197 | self.model.step()
198 |
199 | self.iteration_count += 1
200 |
201 | def episode_finish(self, reason: str):
202 | # log ball's distance to center and velocity at the end of each episode.
203 | log.info(
204 | "Episode {} ends at iter {}, ball dist to target ={}, ball speed={} reason={}".format(
205 | self._episode_count,
206 | self.iteration_count,
207 | self.model.estimated_distance,
208 | self.model.estimated_speed,
209 | reason,
210 | )
211 | )
212 |
213 |
214 | if __name__ == "__main__":
215 | try:
216 | # configuration for talking to server
217 | config = BonsaiClientConfig(argv=sys.argv)
218 | sim = MoabSim(config)
219 | sim.model.reset()
220 | while sim.run():
221 | continue
222 |
223 | except Exception as e:
224 | print(e)
225 |
--------------------------------------------------------------------------------
/moab_tutorial_1.ink:
--------------------------------------------------------------------------------
1 | ###
2 |
3 | # MSFT Bonsai
4 | # Copyright 2021 Microsoft
5 | # This code is licensed under MIT license (see LICENSE for details)
6 |
7 | # Moab Tutorial 1
8 | # This introductory sample demonstrates how to teach a policy for
9 | # controlling a ball on the plate of a "Moab" hardware device.
10 |
11 | # To understand this Inkling better, please follow our tutorial walkthrough:
12 | # https://aka.ms/moab/tutorial1
13 |
14 | ###
15 |
16 | inkling "2.0"
17 |
18 | using Math
19 | using Goal
20 |
21 | # Distances measured in meters
22 | const RadiusOfPlate = 0.1125 # m
23 |
24 | # Velocities measured in meters per sec.
25 | const MaxVelocity = 6.0
26 | const MaxInitialVelocity = 1.0
27 |
28 | # Threshold for ball placement
29 | const CloseEnough = 0.02
30 |
31 | # Default time delta between simulation steps (s)
32 | const DefaultTimeDelta = 0.045
33 |
34 | # Maximum distance per step in meters
35 | const MaxDistancePerStep = DefaultTimeDelta * MaxVelocity
36 |
37 | # State received from the simulator after each iteration
38 | type ObservableState {
39 | # Ball X,Y position
40 | ball_x: number<-MaxDistancePerStep - RadiusOfPlate .. RadiusOfPlate + MaxDistancePerStep>,
41 | ball_y: number<-MaxDistancePerStep - RadiusOfPlate .. RadiusOfPlate + MaxDistancePerStep>,
42 |
43 | # Ball X,Y velocity
44 | ball_vel_x: number<-MaxVelocity .. MaxVelocity>,
45 | ball_vel_y: number<-MaxVelocity .. MaxVelocity>,
46 | }
47 |
48 | # Action provided as output by policy and sent as
49 | # input to the simulator
50 | type SimAction {
51 | # Range -1 to 1 is a scaled value that represents
52 | # the full plate rotation range supported by the hardware.
53 | input_pitch: number<-1 .. 1>, # rotate about x-axis
54 | input_roll: number<-1 .. 1>, # rotate about y-axis
55 | }
56 |
57 | # Per-episode configuration that can be sent to the simulator.
58 | # All iterations within an episode will use the same configuration.
59 | type SimConfig {
60 | # Model initial ball conditions
61 | initial_x: number<-RadiusOfPlate .. RadiusOfPlate>, # in (m)
62 | initial_y: number<-RadiusOfPlate .. RadiusOfPlate>,
63 |
64 | # Model initial ball velocity conditions
65 | initial_vel_x: number<-MaxInitialVelocity .. MaxInitialVelocity>, # in (m/s)
66 | initial_vel_y: number<-MaxInitialVelocity .. MaxInitialVelocity>,
67 |
68 | # Range -1 to 1 is a scaled value that represents
69 | # the full plate rotation range supported by the hardware.
70 | initial_pitch: number<-1 .. 1>,
71 | initial_roll: number<-1 .. 1>,
72 | }
73 |
74 | # Define a concept graph with a single concept
75 | graph (input: ObservableState) {
76 | concept MoveToCenter(input): SimAction {
77 | curriculum {
78 | # The source of training for this concept is a simulator that
79 | # - can be configured for each episode using fields defined in SimConfig,
80 | # - accepts per-iteration actions defined in SimAction, and
81 | # - outputs states with the fields defined in SimState.
82 | source simulator MoabSim (Action: SimAction, Config: SimConfig): ObservableState {
83 | }
84 |
85 | # The objective of training is expressed as a goal with two
86 | # subgoals: don't let the ball fall off the plate, and drive
87 | # the ball to the center of the plate.
88 | goal (State: ObservableState) {
89 | avoid `Fall Off Plate`:
90 | Math.Hypot(State.ball_x, State.ball_y) in Goal.RangeAbove(RadiusOfPlate * 0.8)
91 | drive `Center Of Plate`:
92 | [State.ball_x, State.ball_y] in Goal.Sphere([0, 0], CloseEnough)
93 | }
94 |
95 | training {
96 | # Limit episodes to 250 iterations instead of the default 1000.
97 | EpisodeIterationLimit: 250
98 | }
99 |
100 | lesson `Randomize Start` {
101 | # Specify the configuration parameters that should be varied
102 | # from one episode to the next during this lesson.
103 | scenario {
104 | initial_x: number<-RadiusOfPlate * 0.5 .. RadiusOfPlate * 0.5>,
105 | initial_y: number<-RadiusOfPlate * 0.5 .. RadiusOfPlate * 0.5>,
106 |
107 | initial_vel_x: number<-MaxInitialVelocity * 0.02 .. MaxInitialVelocity * 0.02>,
108 | initial_vel_y: number<-MaxInitialVelocity * 0.02 .. MaxInitialVelocity * 0.02>,
109 |
110 | initial_pitch: number<-0.2 .. 0.2>,
111 | initial_roll: number<-0.2 .. 0.2>,
112 | }
113 | }
114 | }
115 | }
116 | }
117 |
118 | # Special string to hook up the simulator visualizer
119 | # in the web interface.
120 | const SimulatorVisualizer = "/moabviz/"
--------------------------------------------------------------------------------
/moab_tutorial_2.ink:
--------------------------------------------------------------------------------
1 | ###
2 |
3 | # MSFT Bonsai
4 | # Copyright 2021 Microsoft
5 | # This code is licensed under MIT license (see LICENSE for details)
6 |
7 | # Moab Tutorial 2
8 | # This sample demonstrates how to use domain randomization of
9 | # the ball radius to achieve better deployment on hardware.
10 |
11 | # To understand this Inkling better, please follow our tutorial walkthrough:
12 | # https://aka.ms/moab/tutorial2
13 |
14 | ###
15 |
16 | inkling "2.0"
17 |
18 | using Math
19 | using Goal
20 |
21 | # Distances measured in meters
22 | const RadiusOfPlate = 0.1125
23 |
24 | # Velocities measured in meters per sec.
25 | const MaxVelocity = 6.0
26 | const MaxInitialVelocity = 1.0
27 |
28 | # Threshold for ball placement
29 | const CloseEnough = 0.02
30 |
31 | # Default time delta between simulation steps (s)
32 | const DefaultTimeDelta = 0.045
33 |
34 | # Maximum distance per step in meters
35 | const MaxDistancePerStep = DefaultTimeDelta * MaxVelocity
36 |
37 | # Ping-Pong ball constants
38 | const PingPongRadius = 0.020 # m
39 | const PingPongShell = 0.0002 # m
40 |
41 | # State received from the simulator after each iteration
42 | type ObservableState {
43 | # Ball X,Y position
44 | ball_x: number<-MaxDistancePerStep - RadiusOfPlate .. RadiusOfPlate + MaxDistancePerStep>,
45 | ball_y: number<-MaxDistancePerStep - RadiusOfPlate .. RadiusOfPlate + MaxDistancePerStep>,
46 |
47 | # Ball X,Y velocity
48 | ball_vel_x: number<-MaxVelocity .. MaxVelocity>,
49 | ball_vel_y: number<-MaxVelocity .. MaxVelocity>,
50 | }
51 |
52 | # Action provided as output by policy and sent as
53 | # input to the simulator
54 | type SimAction {
55 | # Range -1 to 1 is a scaled value that represents
56 | # the full plate rotation range supported by the hardware.
57 | input_pitch: number<-1 .. 1>, # scalar over plate rotation about x-axis
58 | input_roll: number<-1 .. 1>, # scalar over plate rotation about y-axis
59 | }
60 |
61 | # Per-episode configuration that can be sent to the simulator.
62 | # All iterations within an episode will use the same configuration.
63 | type SimConfig {
64 | # Model initial ball conditions
65 | initial_x: number<-RadiusOfPlate .. RadiusOfPlate>, # in (m)
66 | initial_y: number<-RadiusOfPlate .. RadiusOfPlate>,
67 |
68 | # Model initial ball velocity conditions
69 | initial_vel_x: number<-MaxInitialVelocity .. MaxInitialVelocity>, # in (m/s)
70 | initial_vel_y: number<-MaxInitialVelocity .. MaxInitialVelocity>,
71 |
72 | # Range -1 to 1 is a scaled value that represents
73 | # the full plate rotation range supported by the hardware.
74 | initial_pitch: number<-1 .. 1>,
75 | initial_roll: number<-1 .. 1>,
76 |
77 | # Model configuration
78 | ball_radius: number, # Radius of the ball in (m)
79 | ball_shell: number, # Shell thickness of ball in (m), shell>0, shell<=radius
80 | }
81 |
82 | # Define a concept graph with a single concept
83 | graph (input: ObservableState) {
84 | concept MoveToCenter(input): SimAction {
85 | curriculum {
86 | # The source of training for this concept is a simulator that
87 | # - can be configured for each episode using fields defined in SimConfig,
88 | # - accepts per-iteration actions defined in SimAction, and
89 | # - outputs states with the fields defined in SimState.
90 | source simulator MoabSim (Action: SimAction, Config: SimConfig): ObservableState {
91 | }
92 |
93 | # The objective of training is expressed as a goal with two
94 | # subgoals: don't let the ball fall off the plate, and drive
95 | # the ball to the center of the plate.
96 | goal (State: ObservableState) {
97 | avoid `Fall Off Plate`:
98 | Math.Hypot(State.ball_x, State.ball_y) in Goal.RangeAbove(RadiusOfPlate * 0.9)
99 | drive `Center Of Plate`:
100 | [State.ball_x, State.ball_y] in Goal.Sphere([0, 0], CloseEnough)
101 | }
102 |
103 | training {
104 | # Limit episodes to 250 iterations instead of the default 1000.
105 | EpisodeIterationLimit: 250,
106 | }
107 |
108 | lesson `Domain Randomize` {
109 | # Specify the configuration parameters that should be varied
110 | # from one episode to the next during this lesson.
111 | scenario {
112 | # Configure the initial positions within a reasonable effective radius
113 | initial_x: number<-RadiusOfPlate * 0.6 .. RadiusOfPlate * 0.6>,
114 | initial_y: number<-RadiusOfPlate * 0.6 .. RadiusOfPlate * 0.6>,
115 |
116 | # Configure the initial velocities of the ball
117 | initial_vel_x: number<-MaxInitialVelocity * 0.4 .. MaxInitialVelocity * 0.4>,
118 | initial_vel_y: number<-MaxInitialVelocity * 0.4 .. MaxInitialVelocity * 0.4>,
119 |
120 | # Configure the initial plate angles
121 | initial_pitch: number<-0.2 .. 0.2>,
122 | initial_roll: number<-0.2 .. 0.2>,
123 |
124 | # Domain randomize the ping pong ball parameters
125 | ball_radius: number,
126 | ball_shell: number,
127 | }
128 | }
129 | }
130 | }
131 | }
132 |
133 | # Special string to hook up the simulator visualizer
134 | # in the web interface.
135 | const SimulatorVisualizer = "/moabviz/"
--------------------------------------------------------------------------------
/moab_tutorial_3.ink:
--------------------------------------------------------------------------------
1 | ###
2 |
3 | # MSFT Bonsai
4 | # Copyright 2021 Microsoft
5 | # This code is licensed under MIT license (see LICENSE for details)
6 |
7 | # Moab Tutorial 3
8 | # This sample demonstrates teaching an AI to avoid an obstacle
9 | # while balancing the ball in the center of the plate.
10 | # Obstacles are defined as an x, y coordinate with a radius.
11 |
12 | # To understand this Inkling better, please follow our tutorial walkthrough:
13 | # https://aka.ms/moab/tutorial3
14 |
15 | ###
16 |
17 | inkling "2.0"
18 |
19 | using Math
20 | using Goal
21 |
22 | # Distances measured in meters
23 | const RadiusOfPlate = 0.1125 # m
24 |
25 | # Velocities measured in meters per sec.
26 | const MaxVelocity = 6.0
27 | const MaxInitialVelocity = 1.0
28 |
29 | # Threshold for ball placement
30 | const CloseEnough = 0.02
31 |
32 | # Default time delta between simulation steps (s)
33 | const DefaultTimeDelta = 0.045
34 |
35 | # Maximum distance per step in meters
36 | const MaxDistancePerStep = DefaultTimeDelta * MaxVelocity
37 |
38 | # Cushion value in avoiding obstacle
39 | const Cushion = 0.01
40 |
41 | # Ping-Pong ball constants
42 | const PingPongRadius = 0.020 # m
43 | const PingPongShell = 0.0002 # m
44 |
45 | # Obstacle definitions
46 | const ObstacleRadius = 0.01
47 | const ObstacleLocationX = 0.04
48 | const ObstacleLocationY = 0.04
49 |
50 | # This is the state received from the simulator
51 | # after each iteration.
52 | type SimState {
53 | # Ball X,Y position
54 | ball_x: number<-MaxDistancePerStep - RadiusOfPlate .. RadiusOfPlate + MaxDistancePerStep>,
55 | ball_y: number<-MaxDistancePerStep - RadiusOfPlate .. RadiusOfPlate + MaxDistancePerStep>,
56 |
57 | # Ball X,Y velocity
58 | ball_vel_x: number<-MaxVelocity .. MaxVelocity>,
59 | ball_vel_y: number<-MaxVelocity .. MaxVelocity>,
60 |
61 | # Obstacle data
62 | obstacle_direction: number<-Math.Pi .. Math.Pi>,
63 | obstacle_distance: number<-2.0*RadiusOfPlate .. 2.0*RadiusOfPlate>,
64 | }
65 |
66 | # This is the state sent to the brain as it observed
67 | # after each iteration.
68 | type ObservableState {
69 | # Ball X,Y position
70 | ball_x: number<-RadiusOfPlate .. RadiusOfPlate>,
71 | ball_y: number<-RadiusOfPlate .. RadiusOfPlate>,
72 |
73 | # Ball X,Y velocity
74 | ball_vel_x: number<-MaxVelocity .. MaxVelocity>,
75 | ball_vel_y: number<-MaxVelocity .. MaxVelocity>,
76 | }
77 |
78 | # Action provided as output by policy (and sent as
79 | # input to the simulator)
80 | type SimAction {
81 | # Range -1 to 1 is a scaled value that represents
82 | # the full plate rotation range supported by the hardware.
83 | input_pitch: number<-1 .. 1>, # rotate about x-axis
84 | input_roll: number<-1 .. 1>, # rotate about y-axis
85 | }
86 |
87 | # Per-episode configuration that can be sent to the simulator.
88 | # All iterations within an episode will use the same configuration.
89 | type SimConfig {
90 | initial_x: number<-RadiusOfPlate .. RadiusOfPlate>,
91 | initial_y: number<-RadiusOfPlate .. RadiusOfPlate>,
92 |
93 | initial_vel_x: number<-MaxInitialVelocity .. MaxInitialVelocity>, # in (m/s)
94 | initial_vel_y: number<-MaxInitialVelocity .. MaxInitialVelocity>,
95 |
96 | # Range -1 to 1 is a scaled value that represents
97 | # the full plate rotation range supported by the hardware.
98 | initial_pitch: number<-1 .. 1>,
99 | initial_roll: number<-1 .. 1>,
100 |
101 | # Model configuration
102 | ball_radius: number, # Radius of the ball in (m)
103 | ball_shell: number, # Shell thickness of ball in (m), shell>0, shell<=radius
104 |
105 | # Obstacle configuration
106 | obstacle_radius: number<0.0 .. RadiusOfPlate>,
107 | obstacle_x: number<-RadiusOfPlate .. RadiusOfPlate>,
108 | obstacle_y: number<-RadiusOfPlate .. RadiusOfPlate>,
109 | }
110 |
111 | # Define a concept graph with a single concept
112 | graph (input: ObservableState) {
113 | concept MoveToCenter(input): SimAction {
114 | curriculum {
115 | # The source of training for this concept is a simulator that
116 | # - can be configured for each episode using fields defined in SimConfig,
117 | # - accepts per-iteration actions defined in SimAction, and
118 | # - outputs states with the fields defined in SimState.
119 | source simulator MoabSim (Action: SimAction, Config: SimConfig): SimState {
120 | }
121 |
122 | # The objective of training is expressed as a goal with three
123 | # subgoals: don't let the ball fall off the plate, drive
124 | # the ball to the center of the plate, and avoid obstacle
125 | goal (State: SimState) {
126 | avoid FallOffPlate:
127 | Math.Hypot(State.ball_x, State.ball_y) in Goal.RangeAbove(RadiusOfPlate * 0.9)
128 | drive CenterOfPlate:
129 | [State.ball_x, State.ball_y] in Goal.Sphere([0, 0], CloseEnough)
130 | avoid HitObstacle:
131 | State.obstacle_distance in Goal.RangeBelow(Cushion)
132 | }
133 |
134 | training {
135 | # Limit episodes to 250 iterations instead of the default 1000.
136 | EpisodeIterationLimit: 250
137 | }
138 |
139 | lesson `Balance with Constraint` {
140 | # Specify the configuration parameters that should be varied
141 | # from one episode to the next during this lesson.
142 | scenario {
143 | # Configure the initial positions within a reasonable effective radius
144 | initial_x: number<-RadiusOfPlate * 0.6 .. RadiusOfPlate * 0.6>,
145 | initial_y: number<-RadiusOfPlate * 0.6 .. RadiusOfPlate * 0.6>,
146 |
147 | # Configure the initial velocities of the ball
148 | initial_vel_x: number<-MaxInitialVelocity * 0.4 .. MaxInitialVelocity * 0.4>,
149 | initial_vel_y: number<-MaxInitialVelocity * 0.4 .. MaxInitialVelocity * 0.4>,
150 |
151 | # Configure the initial plate angles
152 | initial_pitch: number<-0.2 .. 0.2>,
153 | initial_roll: number<-0.2 .. 0.2>,
154 |
155 | # Domain randomize the ping pong ball parameters
156 | ball_radius: number,
157 | ball_shell: number,
158 |
159 | # Configure obstacle parameters
160 | obstacle_radius: ObstacleRadius,
161 | obstacle_x: ObstacleLocationX,
162 | obstacle_y: ObstacleLocationY,
163 |
164 | }
165 | }
166 | }
167 | }
168 | }
169 |
170 | # Special string to hook up the simulator visualizer
171 | # in the web interface.
172 | const SimulatorVisualizer = "/moabviz/"
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | jinja2>=2.11
2 | numpy>=1.15.1
3 | pyrr>=0.10.3
4 |
--------------------------------------------------------------------------------
/test_moab_model.py:
--------------------------------------------------------------------------------
1 | """
2 | Unit tests for Moab physics model
3 | """
4 | __copyright__ = "Copyright 2020, Microsoft Corp."
5 |
6 | # pyright: strict
7 |
8 | import math
9 |
10 | from pyrr import Vector3, vector
11 |
12 | from moab_model import MoabModel
13 |
14 | model = MoabModel()
15 |
16 |
17 | def run_for_duration(duration: float):
18 | """ Runs the model without actions for a duration """
19 | # sync the plate position with commanded position
20 | model.update_plate(True)
21 |
22 | # run for duration
23 | elapsed = 0.0 # type: float
24 | while elapsed < duration:
25 | model.step()
26 | elapsed += model.step_time
27 |
28 |
29 | # basic test for heading
30 | def test_heading():
31 | heading = MoabModel.heading_to_point(0.0, 0.0, 1.0, 0.0, 1.0, 0.0)
32 | assert heading == 0.0, "Expected heading to be 0.0 while moving towards point"
33 |
34 | heading = MoabModel.heading_to_point(0.0, 0.0, -1.0, 0.0, 1.0, 0.0)
35 | assert (
36 | heading == math.pi
37 | ), "Expected heading to be math.pi while moving away from point"
38 |
39 | heading = MoabModel.heading_to_point(0.0, 0.0, 0.0, -1.0, 1.0, 0.0)
40 | assert (
41 | heading == -math.pi / 2
42 | ), "Expected heading to be negative while moving to right of point"
43 |
44 | heading = MoabModel.heading_to_point(0.0, 0.0, 0.0, 1.0, 1.0, 0.0)
45 | assert (
46 | heading == math.pi / 2
47 | ), "Expected heading to be positive while moving to left of point"
48 |
49 |
50 | """
51 | Roll tests.
52 |
53 | The start the ball at the center of the plate and then
54 | command the plate to a tilt position and test the ball
55 | position after 1 second. The ball should be in a known location.
56 |
57 | This tests for:
58 | - sign inversions on the axis
59 | - gravity or other mass related constants being off
60 | - differences in axis behavior
61 | """
62 |
63 | # constants for roll tests
64 | ROLL_DIST = 0.1105
65 | ROLL_DIST_LO = ROLL_DIST - 0.0001
66 | ROLL_DIST_HI = ROLL_DIST + 0.0001
67 | TILT = 0.1
68 |
69 | # disable the noise for the unit tests
70 | def model_init(model: MoabModel):
71 | model.plate_noise = 0.0
72 | model.ball_noise = 0.0
73 | model.jitter = 0.0
74 |
75 |
76 | def test_roll_right():
77 | model.reset()
78 | model_init(model)
79 | model.roll = TILT
80 | run_for_duration(1.0)
81 | q = model.ball.x
82 | assert q > ROLL_DIST_LO and q < ROLL_DIST_HI
83 |
84 |
85 | def test_roll_left():
86 | model.reset()
87 | model_init(model)
88 | model.roll = -TILT
89 | run_for_duration(1.0)
90 | q = model.ball.x
91 | assert -q > ROLL_DIST_LO and -q < ROLL_DIST_HI
92 |
93 |
94 | def test_roll_back():
95 | model.reset()
96 | model_init(model)
97 | model.pitch = -TILT
98 | run_for_duration(1.0)
99 | q = model.ball.y
100 | assert q > ROLL_DIST_LO and q < ROLL_DIST_HI
101 |
102 |
103 | def test_roll_front():
104 | model.reset()
105 | model_init(model)
106 | model.pitch = TILT
107 | run_for_duration(1.0)
108 | q = model.ball.y
109 | assert -q > ROLL_DIST_LO and -q < ROLL_DIST_HI
110 |
111 |
112 | """
113 | Tilt tests.
114 |
115 | These test that the command pitch/roll values move the plate to the limits.
116 | """
117 |
118 |
119 | def test_pitch():
120 | model.reset()
121 | model_init(model)
122 | model.pitch = 1.0
123 | run_for_duration(1.0)
124 | assert model.plate_theta_x == model.plate_theta_limit
125 |
126 | model.reset()
127 | model_init(model)
128 | model.pitch = -1.0
129 | run_for_duration(1.0)
130 | assert model.plate_theta_x == -model.plate_theta_limit
131 |
132 |
133 | def test_roll():
134 | model.reset()
135 | model_init(model)
136 | model.roll = 1.0
137 | run_for_duration(1.0)
138 | assert model.plate_theta_y == model.plate_theta_limit
139 |
140 | model.reset()
141 | model_init(model)
142 | model.roll = -1.0
143 | run_for_duration(1.0)
144 | assert model.plate_theta_y == -model.plate_theta_limit
145 |
146 |
147 | """
148 | Coordinate origin transform tests.
149 |
150 | These test that transforming between coordinate systems and back yields the original coordinates.
151 | """
152 | test_vector = Vector3([0.015, 0.020, 0.030])
153 | TOLERANCE = 0.0000000001
154 |
155 |
156 | def test_world_to_plate_to_world():
157 | vec_plate = model.world_to_plate(test_vector.x, test_vector.y, test_vector.z)
158 | result = model.plate_to_world(vec_plate.x, vec_plate.y, vec_plate.z)
159 | delta = vector.length(result - test_vector)
160 | assert delta < TOLERANCE
161 |
162 |
163 | def test_plate_to_world_to_plate():
164 | vec_world = model.plate_to_world(test_vector.x, test_vector.y, test_vector.z)
165 | result = model.world_to_plate(vec_world.x, vec_world.y, vec_world.z)
166 | delta = vector.length(result - test_vector)
167 | assert delta < TOLERANCE
168 |
169 |
170 | if __name__ == "__main__":
171 | test_heading()
172 |
173 | test_roll_right()
174 | test_roll_left()
175 | test_roll_front()
176 | test_roll_back()
177 |
178 | test_roll()
179 | test_pitch()
180 |
181 | test_world_to_plate_to_world()
182 | test_plate_to_world_to_plate()
183 |
--------------------------------------------------------------------------------
/test_moab_perf.py:
--------------------------------------------------------------------------------
1 | """
2 | Unit tests for Moab physics model
3 | """
4 | __copyright__ = "Copyright 2020, Microsoft Corp."
5 |
6 | # pyright: strict
7 |
8 | import time
9 | import os
10 |
11 | from bonsai_common import Schema
12 | from microsoft_bonsai_api.simulator.client import BonsaiClientConfig
13 | from moab_model import MoabModel
14 | from moab_sim import MoabSim
15 |
16 |
17 | def run_model_for_count(count: int):
18 | """ Runs the model without actions for a duration """
19 | # sync the plate position with commanded position
20 | model = MoabModel()
21 | model.reset()
22 | model.update_plate(True)
23 |
24 | model.plate_noise = 0
25 | model.ball_noise = 0
26 | model.jitter = 0
27 |
28 | # run for count
29 | iterations = 0
30 | while iterations < count:
31 | model.step()
32 | iterations += 1
33 |
34 |
35 | def run_sim_for_count(config: Schema, action: Schema, count: int) -> MoabSim:
36 | """
37 | This uses a passed in config to init the sim and then runs
38 | it for a fixed duration with no actions.
39 |
40 | We do not connect to the platform and drive the loop ourselves.
41 | """
42 | service_config = BonsaiClientConfig(workspace="moab", access_key="utah")
43 | sim = MoabSim(service_config)
44 |
45 | # run with no actions for N seconds.
46 | sim.episode_start(config)
47 |
48 | # disable noise
49 | sim.model.plate_noise = 0
50 | sim.model.ball_noise = 0
51 | sim.model.jitter = 0
52 |
53 | iterations = 0
54 | while iterations < count:
55 | sim.episode_step(action)
56 | iterations += 1
57 |
58 | # return the results
59 | return sim
60 |
61 |
62 | """
63 | Speed tests.
64 |
65 | Run the simulation for N runs, M times each.
66 | Average FPS count and assert if it drops below threshold.
67 |
68 | This tests for:
69 | - regressions in simulator performance
70 | """
71 |
72 | # constants for speed tests
73 | MAX_RUNS = 10
74 | MAX_ITER = 250
75 |
76 | # use FPS fail limit from env if it is available
77 | ENV_FPS_FAIL_LIMIT = os.environ.get("MOAB_PERF_TEST_THRESHOLD")
78 | FPS_FAIL_LIMIT = int(ENV_FPS_FAIL_LIMIT) if ENV_FPS_FAIL_LIMIT else 500
79 |
80 |
81 | def test_model_perf():
82 | avg_fps = 0
83 | for _ in range(0, MAX_RUNS):
84 |
85 | start = time.time()
86 | run_model_for_count(MAX_ITER)
87 | end = time.time()
88 |
89 | fps = MAX_ITER / (end - start)
90 | avg_fps = avg_fps + fps
91 | avg_fps /= MAX_RUNS
92 |
93 | print("model average avg fps: ", avg_fps)
94 | assert (
95 | avg_fps > FPS_FAIL_LIMIT
96 | ), "Iteration speed for Model dropped below {} fps.".format(FPS_FAIL_LIMIT)
97 |
98 |
99 | def test_sim_perf():
100 | avg_fps = 0
101 | for _ in range(0, MAX_RUNS):
102 | start = time.time()
103 | run_sim_for_count(
104 | {},
105 | {
106 | "input_pitch": 0.0,
107 | "input_roll": 0.0,
108 | },
109 | MAX_ITER,
110 | )
111 | end = time.time()
112 |
113 | fps = MAX_ITER / (end - start)
114 | avg_fps = avg_fps + fps
115 | avg_fps /= MAX_RUNS
116 |
117 | print("simulator average avg fps: ", avg_fps)
118 | assert (
119 | avg_fps > FPS_FAIL_LIMIT
120 | ), "Iteration speed for Simulator dropped below {} fps.".format(FPS_FAIL_LIMIT)
121 |
122 |
123 | if __name__ == "__main__":
124 | test_model_perf()
125 | test_sim_perf()
126 |
--------------------------------------------------------------------------------
/test_moab_sim.py:
--------------------------------------------------------------------------------
1 | """
2 | Unit tests for Moab simulator
3 | """
4 | __copyright__ = "Copyright 2020, Microsoft Corp."
5 |
6 | # Temporarily set reportIncompatibleMethodOverride to false to work
7 | # around a short-term bug in the typeshed stub builtins file.
8 | # This can be removed at a later time.
9 | # pyright: strict, reportIncompatibleMethodOverride=false
10 |
11 | import math
12 | from typing import Any, Dict, Iterator, TypeVar, cast
13 |
14 | from microsoft_bonsai_api.simulator.client import BonsaiClientConfig
15 | from bonsai_common import Schema
16 | from moab_model import DEFAULT_PLATE_RADIUS
17 | from moab_sim import MoabSim
18 |
19 | _KT = TypeVar("_KT")
20 | _VT = TypeVar("_VT")
21 |
22 |
23 | def run_for_duration(config: Schema, duration: float) -> MoabSim:
24 | """
25 | This uses a passed in config to init the sim and then runs
26 | it for a fixed duration with no actions.
27 |
28 | We do not connect to the platform and drive the loop ourselves.
29 | """
30 | service_config = BonsaiClientConfig(workspace="moab", access_key="utah")
31 | sim = MoabSim(service_config)
32 |
33 | # run with no actions for N seconds.
34 | elapsed = 0.0
35 | sim.episode_start(config)
36 |
37 | # disable noise
38 | sim.model.plate_noise = 0
39 | sim.model.ball_noise = 0
40 | sim.model.jitter = 0
41 |
42 | while elapsed < duration:
43 | sim.episode_step({})
44 | elapsed += sim.model.step_time
45 |
46 | # return the results
47 | return sim
48 |
49 |
50 | """
51 | These test the speed/direction initialization at episode_start
52 | to make sure the behaviors match expectations.
53 |
54 | We use a flat plate and a fixed time so that final `d = vt`
55 | """
56 |
57 | DURATION = 1.0 # s
58 | SPEED = DEFAULT_PLATE_RADIUS / 4 # m/s
59 | DIST = SPEED
60 |
61 |
62 | def close(value: float, target: float = 0.0, epsilon: float = 0.001) -> bool:
63 | """ returns True is value is within epsilon of target """
64 | if (target - epsilon) <= value <= (target + epsilon):
65 | return True
66 | return False
67 |
68 |
69 | def test_zero_speed_direction():
70 | """ test that no velocity and direction results in no motion """
71 | config = {
72 | "target_x": 0.0,
73 | "target_y": 0.0,
74 | "initial_speed": 0.0,
75 | "initial_direction": 0.0,
76 | }
77 |
78 | sim = run_for_duration(config, DURATION)
79 | assert close(sim.model.ball.x) and close(sim.model.ball.y)
80 |
81 |
82 | def test_towards():
83 | """ move towards [0,0] from [-DIST,0] """
84 | config = {
85 | "time_delta": 0.010,
86 | "target_x": 0.0,
87 | "target_y": 0.0,
88 | "initial_x": -DIST,
89 | "initial_speed": SPEED,
90 | "initial_direction": 0.0,
91 | }
92 |
93 | sim = run_for_duration(config, DURATION)
94 | assert close(sim.model.ball.x) and close(sim.model.ball.y)
95 |
96 |
97 | def test_away():
98 | """ move away from target """
99 | config = {
100 | "time_delta": 0.010,
101 | "target_x": -(DIST + 0.0001),
102 | "initial_x": -DIST,
103 | "initial_speed": SPEED,
104 | "initial_direction": math.radians(180),
105 | }
106 |
107 | sim = run_for_duration(config, DURATION)
108 | assert close(sim.model.ball.x), "Ball X coord should be zero"
109 | assert close(sim.model.ball.y), "Ball Y coord should be zero"
110 |
111 |
112 | def test_angle():
113 | """ move from center, 90 degrees rotated from target to the left """
114 | config = {
115 | "time_delta": 0.010,
116 | "target_x": -(DIST),
117 | "target_y": 0,
118 | "initial_x": 0,
119 | "initial_y": 0,
120 | "initial_speed": SPEED,
121 | "initial_direction": math.radians(90),
122 | }
123 |
124 | sim = run_for_duration(config, DURATION)
125 | assert close(sim.model.ball.x), "Ball X coord should be zero"
126 | assert close(sim.model.ball.y, -DIST), "Ball Y coord should be -{}".format(DIST)
127 |
128 |
129 | def test_angle2():
130 | config = {
131 | "target_x": 0,
132 | "target_y": DIST,
133 | "initial_x": 0,
134 | "initial_y": 0,
135 | "initial_speed": SPEED / 10.0,
136 | # just to the left
137 | "initial_direction": math.radians(90.0),
138 | }
139 |
140 | sim = run_for_duration(config, DURATION)
141 | direction = sim.model.state()["estimated_direction"]
142 | assert direction > 0.0, "Direction should be positive"
143 |
144 | # just to the right
145 | config["initial_direction"] = math.radians(-90.0)
146 |
147 | sim = run_for_duration(config, DURATION)
148 | direction = sim.model.state()["estimated_direction"]
149 | assert direction < 0.0, "Direction should be negative"
150 |
151 |
152 | class KeyProbe(Dict[_KT, _VT]):
153 | """
154 | A "dictionary" that checks to see which keys
155 | are queried on it.
156 | """
157 |
158 | def __init__(self):
159 | self.found_keys = set()
160 |
161 | def get(self, key: str, default: _VT) -> _VT:
162 | self.found_keys.add(key)
163 | return default
164 |
165 |
166 | def test_interface():
167 | """
168 | This tests that the state, config and action descriptions
169 | has matching values used by the model.
170 | """
171 | service_config = BonsaiClientConfig(workspace="moab", access_key="utah")
172 | sim = MoabSim(service_config)
173 | model = sim.model
174 | iface = sim.get_interface()
175 | model_state = model.state()
176 |
177 | # state is pull from the model
178 |
179 | fields = cast(Dict[str, Any], iface.description)["state"]["fields"]
180 | state_fields = set(cast(Iterator[str], map(lambda x: x["name"], fields)))
181 |
182 | # interface in state
183 | for attr in state_fields:
184 | if model_state.get(attr) is None:
185 | assert (
186 | False
187 | ), "Unexpected variable {} found in interface State but missing from model State.".format(
188 | attr
189 | )
190 |
191 | # state in interface
192 | for attr in model_state:
193 | if attr not in state_fields:
194 | assert (
195 | False
196 | ), "Unexpected variable {} found in model State, missing from interface State.".format(
197 | attr
198 | )
199 |
200 | # extract config and action from interface
201 | fields = cast(Dict[str, Any], iface.description)["config"]["fields"]
202 | config_fields = set(map(lambda x: x["name"], fields))
203 |
204 | # collect the model config
205 | config_probe = KeyProbe() # type: KeyProbe[str, Any]
206 | sim.episode_start(config_probe)
207 | model_config = config_probe.found_keys
208 |
209 | # interface in config
210 | for attr in config_fields:
211 | if attr not in model_config:
212 | assert (
213 | False
214 | ), "Unexpected variable {} found in Config interface but missing from model Config.".format(
215 | attr
216 | )
217 |
218 | # state in interface
219 | for attr in model_config:
220 | if attr not in config_fields:
221 | assert (
222 | False
223 | ), "Unexpected variable {} found in model Config, missing from Config interface.".format(
224 | attr
225 | )
226 |
227 | # Now do the actions
228 | fields = cast(Dict[str, Any], iface.description)["action"]["fields"]
229 | action_fields = set(cast(Iterator[str], map(lambda x: x["name"], fields)))
230 |
231 | # collect the model actions
232 | action_probe = KeyProbe() # type: KeyProbe[str, Any]
233 | sim.episode_step(action_probe)
234 | model_action = action_probe.found_keys
235 |
236 | # interface in action
237 | for attr in action_fields:
238 | if attr not in model_action:
239 | assert (
240 | False
241 | ), "Unexpected variable {} found in Action interface but missing from model Action.".format(
242 | attr
243 | )
244 |
245 | # state in action
246 | for attr in model_action:
247 | if attr not in action_fields:
248 | assert (
249 | False
250 | ), "Unexpected variable {} found in model Action, missing from Action interface.".format(
251 | attr
252 | )
253 |
254 | pass
255 |
256 |
257 | if __name__ == "__main__":
258 | test_interface()
259 | test_zero_speed_direction()
260 |
261 | test_towards()
262 | test_away()
263 | test_angle()
264 | test_angle2()
265 |
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/microsoft/moabsim-py/54bb72bf47f6c23607f910d0ba352c07f0a7ba73/tests/__init__.py
--------------------------------------------------------------------------------
/tests/conftest.py:
--------------------------------------------------------------------------------
1 | # Helper function for arguments for pytest, with defaults
2 | def pytest_addoption(parser):
3 | parser.addoption("--brain_name", action="store", default="my_brain")
4 | parser.addoption("--brain_version", action="store", default=1)
5 | parser.addoption("--concept_name", action="store", default="MoveToCenter")
6 | parser.addoption("--file_name", action="store", default="assess_config.json")
7 | parser.addoption("--simulator_package_name", action="store", default="Moab")
8 | parser.addoption("--instance_count", action="store", default="20")
9 | parser.addoption("--custom_assess_name", action="store", default="my_custom_assessment")
10 | parser.addoption("--log_analy_workspace", action="store", default=None)
11 | parser.addoption("--inkling_fname", action="store", default="./Machine-Teaching-Examples/model_import/moab-imported-concept.ink")
12 | parser.addoption("--import_name", action="store", default="My ML Model")
13 | parser.addoption("--model_file_path", action="store", default="./Machine-Teaching-Examples/model_import/state_transform_deep.zip")
14 | parser.addoption("--episode_iteration_limit", action="store", default=250)
--------------------------------------------------------------------------------
/tests/test_model_import.py:
--------------------------------------------------------------------------------
1 | """
2 | Pytest for confirming model import assessment works using Telescope data
3 | retrieval from LogAnalyticsDataClient. Usage:
4 |
5 | Train a brain using model import's instructions, then run
6 |
7 | pytest tests/test_model_import.py -s \
8 | --brain_name \
9 | --log_analy_workspace \
10 | --custom_assess_name \
11 | --model_file_path "./Machine-Teaching-Examples/model_import/state_transform_deep.zip"
12 |
13 | or
14 | --model_file_path "./Machine-Teaching-Examples/model_import/state_transform_deep.onnx"
15 | """
16 |
17 | __author__ = "Journey McDowell"
18 | __copyright__ = "Copyright 2021, Microsoft Corp."
19 |
20 | import os
21 | import pytest
22 | import glob
23 | import pandas as pd
24 | import numpy as np
25 | from azure.loganalytics import LogAnalyticsDataClient
26 | from azure.common.credentials import get_azure_cli_credentials
27 | from azure.loganalytics.models import QueryBody
28 | import ast
29 | import json
30 | import matplotlib.pyplot as plt
31 | import time
32 |
33 | # Allowing optional flags to replace defaults for pytest from tests/conftest.py
34 | @pytest.fixture()
35 | def brain_name(pytestconfig):
36 | return pytestconfig.getoption("brain_name")
37 |
38 | @pytest.fixture()
39 | def brain_version(pytestconfig):
40 | return pytestconfig.getoption("brain_version")
41 |
42 | @pytest.fixture()
43 | def concept_name(pytestconfig):
44 | return pytestconfig.getoption("concept_name")
45 |
46 | # json file with assessment configs for input
47 | @pytest.fixture()
48 | def file_name(pytestconfig):
49 | return pytestconfig.getoption("file_name")
50 |
51 | @pytest.fixture()
52 | def simulator_package_name(pytestconfig):
53 | return pytestconfig.getoption("simulator_package_name")
54 |
55 | @pytest.fixture()
56 | def inkling_fname(pytestconfig):
57 | return pytestconfig.getoption("inkling_fname")
58 |
59 | @pytest.fixture()
60 | def instance_count(pytestconfig):
61 | return pytestconfig.getoption("instance_count")
62 |
63 | @pytest.fixture()
64 | def custom_assess_name(pytestconfig):
65 | return pytestconfig.getoption("custom_assess_name")
66 |
67 | @pytest.fixture()
68 | def log_analy_workspace(pytestconfig):
69 | return pytestconfig.getoption("log_analy_workspace")
70 |
71 | @pytest.fixture()
72 | def import_name(pytestconfig):
73 | return pytestconfig.getoption("import_name")
74 |
75 | @pytest.fixture()
76 | def model_file_path(pytestconfig):
77 | return pytestconfig.getoption("model_file_path")
78 |
79 | @pytest.fixture()
80 | def episode_iteration_limit(pytestconfig):
81 | return pytestconfig.getoption("episode_iteration_limit")
82 |
83 | # Use CLI to import a ML model as .onnx or tf
84 | def test_model_import(import_name, model_file_path):
85 | os.system('bonsai importedmodel create --name "{}" --modelfilepath {}'.format(
86 | import_name,
87 | model_file_path,
88 | ))
89 |
90 | os.system('bonsai importedmodel show --name "{}" -o json > status.json'.format(
91 | import_name,
92 | ))
93 |
94 | # Confirm model import succeeded
95 | with open('status.json') as fname:
96 | status = json.load(fname)
97 |
98 | assert status['Status'] == 'Succeeded'
99 |
100 | # Use CLI to create, upload inkling, train, and wait til complete
101 | def test_train_brain(brain_name, brain_version, inkling_fname, simulator_package_name):
102 | os.system('bonsai brain create -n {}'.format(
103 | brain_name,
104 | ))
105 | os.system('bonsai brain version update-inkling -n {} --version {} -f {}'.format(
106 | brain_name,
107 | brain_version,
108 | inkling_fname
109 | ))
110 | concept_names = ['ImportedConcept', 'MoveToCenter']
111 | for concept in concept_names:
112 | time.sleep(20)
113 | if concept == 'ImportedConcept':
114 | os.system('bonsai brain version start-training -n {} --version {} -c {}'.format(
115 | brain_name,
116 | brain_version,
117 | concept
118 | ))
119 | else:
120 | os.system('bonsai brain version start-training -n {} --version {} --simulator-package-name {} -c {}'.format(
121 | brain_name,
122 | brain_version,
123 | simulator_package_name,
124 | concept
125 | ))
126 |
127 | # Do not continue until training is complete
128 | running = True
129 | while running:
130 | time.sleep(20)
131 | os.system('bonsai brain version show --name {} --version {} -o json > status.json'.format(
132 | brain_name,
133 | brain_version,
134 | ))
135 | with open('status.json') as fname:
136 | status = json.load(fname)
137 | if status['trainingState'] == 'Active':
138 | pass
139 | else:
140 | running = False
141 | time.sleep(300)
142 | print('Training complete...')
143 |
144 | # Final check concepts are complete
145 | with open('status.json') as fname:
146 | status = json.load(fname)
147 | assert status['status'] == 'Succeeded'
148 | print('All Concepts trained')
149 |
150 | # Main test function for
151 | # 1. running custom assessment using bonsai-cli
152 | # 2. retrieving data from Log Analytics Workspace using LogAnalyticsDataClient
153 | # 3. flattening states, actions, and configs
154 | # 4. making plots for episode metrics
155 | # 5. qualifying pass/fail
156 | def test_assessment_brain(brain_name, brain_version, concept_name, file_name, simulator_package_name, instance_count, custom_assess_name, log_analy_workspace, episode_iteration_limit):
157 | # Run custom assessment
158 | os.system('bonsai brain version assessment start --brain-name {} --brain-version {} --concept-name {} --file {} --simulator-package-name {} --instance-count {} --name {} --episode-iteration-limit {}'.format(
159 | brain_name,
160 | brain_version,
161 | concept_name,
162 | file_name,
163 | simulator_package_name,
164 | instance_count,
165 | custom_assess_name,
166 | episode_iteration_limit,
167 | ))
168 |
169 | # Do not continue until assessment is complete and waited 5 minutes
170 | running = True
171 | while running:
172 | time.sleep(10)
173 | os.system('bonsai brain version assessment show --brain-name {} --brain-version {} --name {} -o json > status.json'.format(
174 | brain_name,
175 | brain_version,
176 | custom_assess_name
177 | ))
178 | with open('status.json') as fname:
179 | status = json.load(fname)
180 | if status['status'] == 'Completed':
181 | running = False
182 | print('Assessment complete, waiting 5 min for data to appear in LAW...')
183 | for i in range(5):
184 | print('{} min...'.format(5-i))
185 | time.sleep(60)
186 |
187 | # Extract telescope from LAW using workspace ID and return flattened
188 | df = extract_telescope(log_analy_workspace, brain_name, brain_version, custom_assess_name)
189 |
190 | df = df.reset_index(drop=True)
191 |
192 | # Make plots
193 | # EpisodeIndex based on unique EpisodeIds
194 | df['EpisodeIndex'] = np.zeros(len(df))
195 | k = 1
196 | for i in list(set(df['EpisodeId'])):
197 | for j in range(len(df)):
198 | if df['EpisodeId'].iloc[j] == i:
199 | df.at[j,'EpisodeIndex'] = k
200 | k += 1
201 |
202 | # Manipulate
203 | df['distance_to_center'] = np.sqrt(df['ball_x'] ** 2 + df['ball_y'] ** 2)
204 | df['velocity_magnitude'] = np.sqrt(df['ball_vel_x'] ** 2 + df['ball_vel_y'] ** 2)
205 |
206 | # Create dataframe consisting of episode finish metrics
207 | df_last = pd.DataFrame({})
208 | mse_dist_list = []
209 | mse_vel_list = []
210 | for ep in range(1, int(df['EpisodeIndex'].max())+1):
211 | last_iter = df[(df['EpisodeIndex']==ep) & (df['IterationIndex']==len(df[df['EpisodeIndex']==ep]))]
212 | print(last_iter.head())
213 |
214 | mse_dist_list.append(np.square(np.subtract(0, df[(df['EpisodeIndex']==ep)]['distance_to_center'])).mean())
215 | mse_vel_list.append(np.square(np.subtract(0, df[(df['EpisodeIndex']==ep)]['velocity_magnitude'])).mean())
216 |
217 | df_last = pd.concat([df_last, last_iter], sort=False)
218 |
219 | df_last['mse_dist'] = mse_dist_list
220 | df_last['mse_vel'] = mse_vel_list
221 |
222 | # Create dataframe consisting of summary info
223 | df_summary = {}
224 | df_summary['percentage_full_episodes'] = (len(df[df['IterationIndex']==251]) / df['EpisodeIndex'].max()) * 100
225 | df_summary['avg_final_distance_to_center'] = np.mean(df_last['distance_to_center'])
226 | df_summary['avg_final_velocity_magnitude'] = np.mean(df_last['velocity_magnitude'])
227 | df_summary['mse_dist_total'] = np.square(np.subtract(0, df['distance_to_center'])).mean()
228 | df_summary['mse_vel_total'] = np.square(np.subtract(0, df['velocity_magnitude'])).mean()
229 |
230 | # Save Summary values as json
231 | with open('brain_summary.json', 'w') as outfile:
232 | json.dump(df_summary, outfile)
233 |
234 | # Plot Final Values
235 | fig, ax = plt.subplots(1, 1, figsize=(16, 8))
236 | episodes = [i for i in range(1, int(df['EpisodeIndex'].max()+1))]
237 | ax.plot(episodes, df_last['distance_to_center'])
238 | ax.plot(episodes, df_last['velocity_magnitude'])
239 | ax.legend(['Final Distance to Center', 'Final Velocity Mag'])
240 | ax.set_xlabel('Custom Assessment Episodes')
241 | fig.suptitle('brain \n Percentage of Full Episodes: {:0.2f}% \n Average Final Ball Distance To Center: {:0.4f} \n Average Final Ball Velocity Mag: {:0.4f} \n'.format(df_summary['percentage_full_episodes'], df_summary['avg_final_distance_to_center'], df_summary['avg_final_velocity_magnitude']), fontsize=14)
242 |
243 | # Plot MSE Values
244 | figa, axa = plt.subplots(1, 1, figsize=(16, 8))
245 | axa.plot(episodes, df_last['mse_dist'])
246 | axa.plot(episodes, df_last['mse_vel'])
247 | axa.legend(['MSE Distance', 'MSE Vel Mag'])
248 | axa.set_xlabel('Custom Assessment Episodes')
249 | figa.suptitle('brain \n Percentage of Full Episodes: {:0.2f}% \n Average MSE Ball Distance To Center: {:0.4f} \n Average MSE Ball Velocity Mag: {:0.4f} \n'.format(df_summary['percentage_full_episodes'], df_summary['mse_dist_total'], df_summary['mse_vel_total']), fontsize=14)
250 |
251 |
252 | print('~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~')
253 | for key, val in df_summary.items():
254 | print(key, val)
255 | print('~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~')
256 | #plt.show()
257 |
258 | # Assert tests for qualification
259 | assert df_summary['percentage_full_episodes'] >= 50
260 | assert df_summary['avg_final_distance_to_center'] <= 0.1
261 | assert df_summary['avg_final_velocity_magnitude'] <= 0.1
262 | assert df_summary['mse_dist_total'] <= 0.008
263 | assert df_summary['mse_vel_total'] <= 0.008
264 |
265 | # Extract telescope data using query
266 | @pytest.mark.skip(reason="helper")
267 | def extract_telescope(log_analy_workspace_id, brain_name, brain_version, assessment_name):
268 | creds, _ = get_azure_cli_credentials(resource="https://api.loganalytics.io")
269 | log_client = LogAnalyticsDataClient(creds)
270 | myWorkSpaceId = log_analy_workspace_id
271 | result = log_client.query(myWorkSpaceId, QueryBody(**{
272 | 'query': (
273 | 'EpisodeLog_CL'
274 | '| where BrainName_s == "{}" and BrainVersion_d == "{}" and AssessmentName_s == "{}"'
275 | '| where TimeGenerated > ago(30d)'
276 | '| join kind=inner ('
277 | 'IterationLog_CL'
278 | '| sort by Timestamp_t desc'
279 | ') on EpisodeId_g'
280 | '| project AssessmentName = AssessmentName_s, EpisodeId = EpisodeId_g, IterationIndex = IterationIndex_d, Timestamp = Timestamp_t, SimState = parse_json(SimState_s), SimAction = parse_json(SimAction_s), Reward = Reward_d, CumulativeReward = CumulativeReward_d, Terminal = Terminal_b, LessonIndex = LessonIndex_d, SimConfig = parse_json(SimConfig_s), GoalMetrics = parse_json(GoalMetrics_s), EpisodeType = EpisodeType_s, FinishReason = FinishReason_s'
281 | '| order by EpisodeId asc, IterationIndex asc'
282 | ).format(brain_name, str(brain_version), assessment_name)
283 | }))
284 |
285 | df = pd.DataFrame(result.tables[0].rows, columns=[result.tables[0].columns[i].name for i in range(len(result.tables[0].columns))])
286 |
287 | df_flattened = format_kql_logs(df)
288 | df_flattened.to_csv('flattened_telescope.csv')
289 | return df_flattened
290 |
291 | # Flatten data
292 | @pytest.mark.skip(reason="helper")
293 | def format_kql_logs(df: pd.DataFrame) -> pd.DataFrame:
294 | ''' Function to format a dataframe obtained from KQL query.
295 | Output format: keeps only selected columns, and flatten nested columns [SimAction, SimState, SimConfig]
296 | Parameters
297 | ----------
298 | df : DataFrame
299 | dataframe obtained from running KQL query then exporting `_kql_raw_result_.to_dataframe()`
300 | '''
301 | selected_columns = ["Timestamp", "IterationIndex", "Reward", "CumulativeReward", "Terminal", "SimState", "SimAction", "SimConfig", "EpisodeId"]
302 | nested_columns = ["SimState", "SimAction", "SimConfig"]
303 | df_selected_columns = df[selected_columns]
304 | series_lst = []
305 | ordered_columns = ["EpisodeId", "IterationIndex", "Reward", "Terminal"]
306 | for i in nested_columns:
307 | try:
308 | new_series = df_selected_columns[i].apply(ast.literal_eval).apply(pd.Series)
309 | except:
310 | df_selected_columns[i].fillna(value=str({key: str(np.nan) for key, value in ast.literal_eval(df_selected_columns[i][1]).items()}), inplace=True)
311 | new_series = df_selected_columns[i].apply(ast.literal_eval).apply(pd.Series)
312 | column_names = new_series.columns.values.tolist()
313 | series_lst.append(new_series)
314 | if len(column_names) > 0:
315 | ordered_columns.extend(column_names)
316 | del(df_selected_columns[i])
317 |
318 | series_lst.append(df_selected_columns)
319 | formated_df = pd.concat(series_lst, axis=1)
320 | formated_df = formated_df.sort_values(by='Timestamp',ascending=True) # reorder df based on Timestamp
321 | formated_df.index = range(len(formated_df)) # re-index
322 | formated_df['Timestamp']=pd.to_datetime(formated_df['Timestamp']) # convert Timestamp to datetime
323 |
324 | formated_df = formated_df[ordered_columns]
325 | return formated_df.sort_values(by=["EpisodeId", "IterationIndex"])
--------------------------------------------------------------------------------