Urban Drone Demo

Copyright © 2024, United States Government, as represented by the Administrator of the National Aeronautics and Space Administration. All rights reserved.

The “"Fault Model Design tools - fmdtools version 2"” software is licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.

Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
[1]:
import fmdtools.sim.propagate as propagate
from fmdtools.analyze import tabulate

import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
import pandas as pd
import numpy as np

Model Overview

The drone model is defined in drone_mdl_urban.py, along with some visualization functions.

[2]:
from drone_mdl_urban import Drone
[3]:
mdl = Drone()

This is the model structure:

[4]:
mg = mdl.as_modelgraph()
fig, ax = mg.draw()
../../_images/examples_multirotor_Urban_Drone_Demo_6_0.png

We can also view the grid environment using its show methods:

[5]:
collections={"all_occupied": {"color": "red"}, "all_allowed": {"color": "blue"}, "start": {"color": "blue"}, "end": {"color": "blue"}}
fig, ax = mdl.flows['environment'].c.show({"height": {}}, collections=collections)
../../_images/examples_multirotor_Urban_Drone_Demo_8_0.png

Which shows the Start, End, and allowed/unsafe locations in the 1000x1000-m grid. In this display, line thickness corresponds to building height, and hatching corresponds to whether or not the space is occupied. We can also display this using show.coord3d:

[6]:
fig, ax = mdl.flows['environment'].c.show_z("height", collections=collections)
../../_images/examples_multirotor_Urban_Drone_Demo_10_0.png

Nominal Simulation

Below we show how this drone performs in the nominal scenario.

[7]:
results_nom, hist_nom =propagate.nominal(mdl)
fig, axs = hist_nom.plot_line("fxns.store_ee.s.soc",
                            'flows.ee_1.s.rate',
                            'flows.dofs.s.z',
                            'fxns.plan_path.m.mode',
                            'fxns.plan_path.s.pt',
                            'flows.des_traj.s.power')
../../_images/examples_multirotor_Urban_Drone_Demo_12_0.png

As shown, the flight ends fairly quickly (in 10 minutes), with the drone successively proceeding through points in the flight plan.

We can also view this flightpath in 3-d space using History.plot_trajectories:

[8]:
fig, ax = hist_nom.plot_trajectories('dofs.s.x', 'dofs.s.y', 'dofs.s.z')
../../_images/examples_multirotor_Urban_Drone_Demo_14_0.png

Trajectory plots can be overlaid on top of environment plots. In this case, we defined the method plot_env_with_traj and plot_env_with_traj_z for this case.

[9]:
from drone_mdl_urban import plot_env_with_traj, plot_env_with_traj_z

[10]:
plot_env_with_traj_z(hist_nom, mdl)
[10]:
(<Figure size 400x400 with 1 Axes>,
 <Axes3D: title={'center': 'trajectory'}, xlabel='x', ylabel='y'>)
../../_images/examples_multirotor_Urban_Drone_Demo_17_1.png
[11]:
plot_env_with_traj(hist_nom, mdl)
[11]:
(<Figure size 500x500 with 2 Axes>,
 <Axes: title={'center': 'trajectory'}, xlabel='x', ylabel='y'>)
../../_images/examples_multirotor_Urban_Drone_Demo_18_1.png

As shown, this is a rather simple straight-line path. If we wanted a more complex scenario, we could make the path more complex by adding multiple destinations or planning the path based on allowed flight/landing locations.

As it is, we may also want to adjust the timestep/speed to get more resolution, since the drone only has a few discrete timesteps in the air.

The results for the simulation are:

[12]:
results_nom
[12]:
endclass:
--rate:                              1.0
--cost:                              0.0
--expected_cost:                     0.0
--repcost:                             0
--unsafe_flight_time:                  0
--body_strikes:                      0.0
--head_strikes:                      0.0
--property_restrictions:               0
--safecost:                          0.0
--landcost:                            0
--p_safety:                          0.0
--severities: {'hazardous': 0.0, 'minor': 1.0}

Resilience model

A number of different faults have been implemented in the system.

For example, here we inject a mechanical fault in the left-rear rotor during flight:

[13]:
mdl.fxns['affect_dof'].m.faultmodes
[13]:
{'lf_short': Fault(prob=1.0000000000000002e-06, cost=200, phases={}, units='sim'),
 'lf_openc': Fault(prob=1.0000000000000002e-06, cost=200, phases={}, units='sim'),
 'lf_ctlup': Fault(prob=2.0000000000000003e-06, cost=500, phases={}, units='sim'),
 'lf_ctldn': Fault(prob=2.0000000000000003e-06, cost=500, phases={}, units='sim'),
 'lf_ctlbreak': Fault(prob=2.0000000000000003e-06, cost=1000, phases={}, units='sim'),
 'lf_mechbreak': Fault(prob=1.0000000000000002e-06, cost=500, phases={}, units='sim'),
 'lf_mechfriction': Fault(prob=5.000000000000001e-07, cost=500, phases={}, units='sim'),
 'lf_propwarp': Fault(prob=1.0000000000000001e-07, cost=200, phases={}, units='sim'),
 'lf_propstuck': Fault(prob=2.0000000000000002e-07, cost=200, phases={}, units='sim'),
 'lf_propbreak': Fault(prob=3.0000000000000004e-07, cost=200, phases={}, units='sim'),
 'lr_short': Fault(prob=1.0000000000000002e-06, cost=200, phases={}, units='sim'),
 'lr_openc': Fault(prob=1.0000000000000002e-06, cost=200, phases={}, units='sim'),
 'lr_ctlup': Fault(prob=2.0000000000000003e-06, cost=500, phases={}, units='sim'),
 'lr_ctldn': Fault(prob=2.0000000000000003e-06, cost=500, phases={}, units='sim'),
 'lr_ctlbreak': Fault(prob=2.0000000000000003e-06, cost=1000, phases={}, units='sim'),
 'lr_mechbreak': Fault(prob=1.0000000000000002e-06, cost=500, phases={}, units='sim'),
 'lr_mechfriction': Fault(prob=5.000000000000001e-07, cost=500, phases={}, units='sim'),
 'lr_propwarp': Fault(prob=1.0000000000000001e-07, cost=200, phases={}, units='sim'),
 'lr_propstuck': Fault(prob=2.0000000000000002e-07, cost=200, phases={}, units='sim'),
 'lr_propbreak': Fault(prob=3.0000000000000004e-07, cost=200, phases={}, units='sim'),
 'rf_short': Fault(prob=1.0000000000000002e-06, cost=200, phases={}, units='sim'),
 'rf_openc': Fault(prob=1.0000000000000002e-06, cost=200, phases={}, units='sim'),
 'rf_ctlup': Fault(prob=2.0000000000000003e-06, cost=500, phases={}, units='sim'),
 'rf_ctldn': Fault(prob=2.0000000000000003e-06, cost=500, phases={}, units='sim'),
 'rf_ctlbreak': Fault(prob=2.0000000000000003e-06, cost=1000, phases={}, units='sim'),
 'rf_mechbreak': Fault(prob=1.0000000000000002e-06, cost=500, phases={}, units='sim'),
 'rf_mechfriction': Fault(prob=5.000000000000001e-07, cost=500, phases={}, units='sim'),
 'rf_propwarp': Fault(prob=1.0000000000000001e-07, cost=200, phases={}, units='sim'),
 'rf_propstuck': Fault(prob=2.0000000000000002e-07, cost=200, phases={}, units='sim'),
 'rf_propbreak': Fault(prob=3.0000000000000004e-07, cost=200, phases={}, units='sim'),
 'rr_short': Fault(prob=1.0000000000000002e-06, cost=200, phases={}, units='sim'),
 'rr_openc': Fault(prob=1.0000000000000002e-06, cost=200, phases={}, units='sim'),
 'rr_ctlup': Fault(prob=2.0000000000000003e-06, cost=500, phases={}, units='sim'),
 'rr_ctldn': Fault(prob=2.0000000000000003e-06, cost=500, phases={}, units='sim'),
 'rr_ctlbreak': Fault(prob=2.0000000000000003e-06, cost=1000, phases={}, units='sim'),
 'rr_mechbreak': Fault(prob=1.0000000000000002e-06, cost=500, phases={}, units='sim'),
 'rr_mechfriction': Fault(prob=5.000000000000001e-07, cost=500, phases={}, units='sim'),
 'rr_propwarp': Fault(prob=1.0000000000000001e-07, cost=200, phases={}, units='sim'),
 'rr_propstuck': Fault(prob=2.0000000000000002e-07, cost=200, phases={}, units='sim'),
 'rr_propbreak': Fault(prob=3.0000000000000004e-07, cost=200, phases={}, units='sim')}
[14]:
results_fault, hist_fault =propagate.one_fault(mdl, "affect_dof", "lr_mechbreak", time=2.0)
[15]:
fig, axs = hist_fault.plot_line("fxns.store_ee.s.soc",
                                'flows.ee_1.s.rate',
                                'flows.dofs.s.z',
                                'fxns.plan_path.m.mode',
                                'fxns.plan_path.s.pt',
                                'flows.des_traj.s.power')
../../_images/examples_multirotor_Urban_Drone_Demo_24_0.png
[16]:
plot_env_with_traj_z(hist_fault, mdl)
[16]:
(<Figure size 400x400 with 1 Axes>,
 <Axes3D: title={'center': 'trajectory'}, xlabel='x', ylabel='y'>)
../../_images/examples_multirotor_Urban_Drone_Demo_25_1.png
[17]:
plot_env_with_traj(hist_fault, mdl)
[17]:
(<Figure size 500x500 with 2 Axes>,
 <Axes: title={'center': 'trajectory'}, xlabel='x', ylabel='y'>)
../../_images/examples_multirotor_Urban_Drone_Demo_26_1.png
[18]:
results_fault
[18]:
endclass.rate:    1.0000000000000002e-06
endclass.cost:                   28600.0
endclass.expected_cost: 2860.0000000000005
endclass.repcost:                    500
endclass.unsafe_flight_time:         281
endclass.body_strikes:               0.0
endclass.head_strikes:               0.0
endclass.property_restrictions:        0
endclass.safecost:               28100.0
endclass.landcost:                     0
endclass.p_safety:                   0.0
endclass.severities: {'hazardous': 0.0, 'minor': 1.0000000000000002e-06}

Here we inject a large list of faults in the system and evaluate their relative consequences in terms of metrics calculated in find_classification:

[19]:
from fmdtools.analyze.phases import PhaseMap, from_hist
phasemaps = from_hist(hist_nom, fxn_modephases=[])
phasemaps
[19]:
{'ctl_dof': PhaseMap({'nominal': [0.0, 30.0]}, {}),
 'plan_path': PhaseMap({'taxi': [0.0, 1.0], 'move': [1.1, 1.6], 'land': [1.7, 2.4], 'taxi1': [2.5, 30.0]}, {})}
[20]:
from drone_mdl_urban import make_move_quad
move_quad=make_move_quad(hist_nom, phasemaps['plan_path'].phases['move'])
move_quad
[20]:
{'samp': 'quadrature',
 'quad': {'nodes': [-0.20000000000000018, 1.0], 'weights': [1.0, 0.92]}}
[21]:
from fmdtools.sim.sample import FaultDomain, FaultSample


fd = FaultDomain(mdl)
fd.add_all()

fs = FaultSample(fd, phasemap = phasemaps['plan_path'])
fs.add_fault_phases("move", method = "quad",
                    args=(move_quad['quad']['nodes'], move_quad['quad']['weights']))

fs
[21]:
FaultSample of scenarios:
 - manage_health_lostfunction_t2
 - manage_health_lostfunction_t2
 - store_ee_nocharge_t2
 - store_ee_nocharge_t2
 - store_ee_lowcharge_t2
 - store_ee_lowcharge_t2
 - store_ee_s1p1_short_t2
 - store_ee_s1p1_short_t2
 - store_ee_s1p1_degr_t2
 - store_ee_s1p1_degr_t2
 - ... (118 total)
[22]:
endresults, hists = propagate.fault_sample(mdl, fs, staged=False)
SCENARIOS COMPLETE:  66%|██████▌   | 78/118 [04:54<02:31,  3.78s/it]
Error at t=11.2 in scenario SingleFaultScenario(sequence={2.0: Injection(faults={'affect_dof': ['rf_propstuck']}, disturbances={})}, times=(2,), function='affect_dof', fault='rf_propstuck', rate=1.0416666666666668e-07, name='affect_dof_rf_propstuck_t2', time=2, phase='move')

---------------------------------------------------------------------------
KeyboardInterrupt                         Traceback (most recent call last)
Cell In[22], line 1
----> 1 endresults, hists = propagate.fault_sample(mdl, fs, staged=False)

File ~\Documents\GitHub\fmdtools\fmdtools\sim\propagate.py:631, in fault_sample(mdl, fs, include_nominal, get_phasemap, **kwargs)
    628 nomresult, nomhist, nomscen, c_mdl, t_end_nom = n_outs
    629 scenlist = fs.scenarios()
--> 631 results, mdlhists = scenlist_helper(mdl,
    632                                     scenlist,
    633                                     c_mdl,
    634                                     **kwargs,
    635                                     nomhist=nomhist,
    636                                     nomresult=nomresult)
    638 if include_nominal:
    639     process_nominal(mdlhists, nomhist, results, nomresult, t_end_nom, **kwargs)

File ~\Documents\GitHub\fmdtools\fmdtools\sim\propagate.py:803, in scenlist_helper(mdl, scenlist, c_mdl, **kwargs)
    801         else:
    802             mdl_i = c_mdl[0].new()
--> 803         ec, mh, t_end = exec_scen(mdl_i, scen, indiv_id=str(i), **kwargs)
    804         results[name], mdlhists[name] = ec, mh
    805 return results, mdlhists

File ~\Documents\GitHub\fmdtools\fmdtools\sim\propagate.py:839, in exec_scen(mdl, scen, save_args, indiv_id, **kwargs)
    821 def exec_scen(mdl, scen, save_args={}, indiv_id='', **kwargs):
    822     """
    823     Executes a scenario and generates results and classifications given a model and
    824     nominal model history.
   (...)
    837         :data:`sim_kwargs` for :func:`prop_one_scen`
    838     """
--> 839     result, mdlhist, _, t_end, = prop_one_scen(mdl, scen, **kwargs)
    840     save_helper(save_args, result, mdlhist, indiv_id=indiv_id, result_id=str(scen.name))
    841     return result, mdlhist, t_end

File ~\Documents\GitHub\fmdtools\fmdtools\sim\propagate.py:1105, in prop_one_scen(mdl, scen, ctimes, nomhist, nomresult, **kwargs)
   1103     disturbances = {}
   1104 try:
-> 1105     mdl.propagate(t, fxnfaults, disturbances, run_stochastic=run_stochastic)
   1106 except Exception as e:
   1107     raise Exception("Error in scenario " + str(scen)) from e

File ~\Documents\GitHub\fmdtools\fmdtools\define\architecture\function.py:780, in FunctionArchitecture.propagate(self, time, fxnfaults, disturbances, run_stochastic)
    778 # Step 2: Run Static Propagation Methods
    779 try:
--> 780     self.prop_static(time, run_stochastic=run_stochastic)
    781 except Exception as e:
    782     raise Exception("Error in static propagation at time t=" + str(time)) from e

File ~\Documents\GitHub\fmdtools\fmdtools\define\architecture\function.py:809, in FunctionArchitecture.prop_static(self, time, run_stochastic)
    806 flows_to_check = {*self.staticflows}
    807 for fxnname in list(activefxns).copy():
    808     # Update functions with new values, check to see if new faults or states
--> 809     oldmutables = self.fxns[fxnname].return_mutables()
    810     self.fxns[fxnname]('static', time=time, run_stochastic=run_stochastic)
    811     if oldmutables != self.fxns[fxnname].return_mutables():

File ~\Documents\GitHub\fmdtools\fmdtools\define\object\base.py:637, in BaseObject.return_mutables(self)
    625 def return_mutables(self):
    626     """
    627     Return all mutable values in the block.
    628
   (...)
    634         tuple of all mutable roles for the object.
    635     """
    636     return tuple([mut.return_mutables() if hasattr(mut, 'return_mutables')
--> 637                   else mut for mut in self.find_mutables()])

File ~\Documents\GitHub\fmdtools\fmdtools\define\object\base.py:622, in BaseObject.find_mutables(self)
    620 def find_mutables(self):
    621     """Return list of mutable roles."""
--> 622     return [v for v in self.get_roles_as_dict(with_immutable=False).values()
    623             if not ismethod(v)]

File ~\Documents\GitHub\fmdtools\fmdtools\define\object\base.py:501, in BaseObject.get_roles_as_dict(self, with_immutable, with_prefix, flex_prefixes, with_flex, no_flows, *roletypes, **kwargs)
    498 if not non_flex_roletypes:
    499     non_flex_roletypes = 'none'
--> 501 roles = self.get_roles(*non_flex_roletypes,
    502                        with_immutable=with_immutable,
    503                        no_flows=no_flows)
    504 non_flex_roles = {role: getattr(self, role) for role in roles}
    505 all_roles = {**flex_roles, **non_flex_roles}

File ~\Documents\GitHub\fmdtools\fmdtools\define\object\base.py:461, in BaseObject.get_roles(self, with_immutable, no_flows, *roletypes, **kwargs)
    459 """Get all roles."""
    460 roletypes = self.get_default_roletypes(*roletypes, no_flows=no_flows)
--> 461 return [role for roletype in roletypes
    462         for role in getattr(self, roletype+'s', [])
    463         if with_immutable or role not in self.immutable_roles]

File ~\Documents\GitHub\fmdtools\fmdtools\define\object\base.py:461, in <listcomp>(.0)
    459 """Get all roles."""
    460 roletypes = self.get_default_roletypes(*roletypes, no_flows=no_flows)
--> 461 return [role for roletype in roletypes
    462         for role in getattr(self, roletype+'s', [])
    463         if with_immutable or role not in self.immutable_roles]

KeyboardInterrupt:
[23]:
plot_env_with_traj_z(hists , mdl)
[24]:
statsfmea = tabulate.FMEA(endresults, fs,
                          weight_metrics=['rate'],
                          avg_metrics=['unsafe_flight_time', 'cost', 'repcost',
                                       'landcost', 'body_strikes',
                                       'head_strikes', 'property_restrictions'])
fmeatab = statsfmea.as_table(sort_by="cost")
fmeatab
[ ]: