Stochastic Modelling in fmdtools

This notebook covers the basics of stochastic modelling in fmdtools. Stochastic models are models which run non-determinsitically, meaning that simulating the model at the same inputs results in different outputs because of randomness or uncertainty in the model behavior. Thus, to determine the distribution of outcomes which may result from a simulation, a stochastic model must be run a number of times. This notebook will cover: - How to construct a stochastic model by adding stochastic states to function blocks and incorporating them in function behavior - How to simulate single scenarios and distributions of scenarios in propagate methods - How to visualize and analyze the results of stochastic model simulations

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]:
from fmdtools.sim.sample import ParameterSample, ParameterDomain
import fmdtools.sim.propagate as propagate

import inspect # we will be using inspect to look at

This notebook uses Pump model in pump_stochastic.py, which is an adaptation of the original ex_pump.py model with stochastic states added.

[2]:
from pump_stochastic import Pump

Below is the structure, with the same functions/flows as before.

[3]:
mg = Pump().as_modelgraph()
fig, ax = mg.draw()
../../_images/examples_pump_Stochastic_Modelling_5_0.png

Model Setup

This model has been augmented stochastic states and behaviors to enable stochastic simulation using the Rand class.

This is what a Rand object looks like:

[4]:
from fmdtools.define.container.rand import Rand
r = Rand()
r
[4]:
Rand(rng=Generator(PCG64) at 0x16D3399E500, probs=[], probdens=1.0, seed=42, run_stochastic=False)

Below, ImportEERand container is used in the ImportEE function to add two random states (via ImportEERandState): - effstate, the quality of the input voltage (i.e. large fluctuations from a step change in power) - grid_noise, which is meant to be fluctuations in voltage from the power source (i.e., more ordinary noise)

This is then reflected in the behavior, which is defined in the Rand.set_rand_state method.

[5]:
help(Rand.set_rand_state)
Help on function set_rand_state in module fmdtools.define.container.rand:

set_rand_state(self, statename, methodname, *args)
    Update the given random state with a given method and arguments.

    (if in run_stochastic mode)

    Parameters
    ----------
    statename : str
        name of the random state defined
    methodname :
        str name of the numpy method to call in the rng
    *args : args
        arguments for the numpy method

In this method,

  • statename is the name of the random state, while

  • methodname corresponds to the name of a random distribution to update the state from. These distributions are pulled from a numpy random number generator (see numpy documentation) and include most one might need including normal, beta, uniform, etc.

  • *args refers to the corresponding arguments to call the given method with, which should be pulled from the corresponding documentation. Below, we have points where this is called.

Belo we show how this is used in the model:

[6]:
from pump_stochastic import ImportEERand, ImportEERandState, ImportEE
[7]:
print(inspect.getsource(ImportEERand))
print(inspect.getsource(ImportEERandState))
class ImportEERand(Rand, copy_default=True):
    s: ImportEERandState = ImportEERandState()

class ImportEERandState(State):
    effstate: float = 1.0
    grid_noise: float = 1.0

In this setup, the effstate and grid_noise states are to be set by the rng in ImportEERand. Below shows how this is embodied in behavior:

[8]:
print(inspect.getsource(ImportEE.static_behavior))
    def static_behavior(self, time):
        self.set_faults()
        if self.m.has_fault('no_v'):
            self.r.s.effstate = 0.0  # an open circuit means no voltage is exported
        elif self.m.has_fault('inf_v'):
            self.r.s.effstate = 100.0  # a voltage spike means voltage is much higher
        else:
            if time > self.t.time:
                self.r.set_rand_state('effstate', 'triangular', 0.9, 1, 1.1)
        if time > self.t.time:
            self.r.set_rand_state('grid_noise', 'normal',
                                  1, 0.05*(2+np.sin(np.pi/2*time)))
        self.ee_out.s.voltage = self.r.s.grid_noise*self.r.s.effstate * 500

As shown, set_rand_state is used:

Move Water Example

The Move Water function has a similar setup, except the stochasticity is auto-updated at each timestep. This is done by providing an eff_update class varaible to the RandState.

[9]:
from pump_stochastic import MoveWat, MoveWatRandState, MoveWatRand
[10]:
print(inspect.getsource(MoveWatRandState))
class MoveWatRandState(State):
    eff: float = 1.0
    eff_update = ('normal', (1.0, 0.2))

In this case, the normal distribution will be drawn from with mean 1.0 and standard deviation 0.2.

The advantage of this is that simple stochastic behaviors do not need to be defined in the behavior method, however, it is less flexible than the previous approach, since one is limited to always drawing from the same distribution at each time-step. The corresponding behavior method (below) thus has no set_rand_state call, since the state eff is automaticall updated outside the behavior definition.

[11]:
print(inspect.getsource(MoveWat.static_behavior))
    def static_behavior(self, time):
        self.s.eff = self.r.s.eff
        super().static_behavior(time)
        if time > self.t.time:
            self.s.inc(total_flow=self.wat_out.s.flowrate)

Model Considerations

To get a distribution of stochastic behavior, the simulation must be run over a number of different random seeds. This seed parameter ensures that each simulation in the same random thread will produce the same results, and that results can be replicated from stochastic simulation by running the model with the same seed.

By default, the seed for the Rand in each of the functions is set to match the Model Rand, meaning that passing a sead at the Model level sets it in all the containing functions also. This can be set by passing an r parameter to the Model.

[12]:
mdl1 = Pump(r={'seed':23})
mdl1.fxns['move_water'].r
[12]:
MoveWatRand(rng=Generator(PCG64) at 0x16D33A02180, probs=[], probdens=1.0, seed=23, run_stochastic=False, s=MoveWatRandState(eff=1.0))
[13]:
mdl2 = Pump(r={'seed':10})
mdl2.fxns['move_water'].r
[13]:
MoveWatRand(rng=Generator(PCG64) at 0x16D33A02C00, probs=[], probdens=1.0, seed=10, run_stochastic=False, s=MoveWatRandState(eff=1.0))

Simulation and Analysis

With this model set up, we can now simulate it using the methods in propagate.

First, let’s simulate it in the nominal scenario:

[14]:
mdl = Pump()
endclass, mdlhist = propagate.nominal(mdl)
fig, axs = mdlhist.plot_line('fxns.import_ee.r.s.effstate',
                            'fxns.import_ee.r.s.grid_noise',
                            'fxns.import_signal.r.s.sig_noise',
                            'fxns.move_water.r.s.eff')
../../_images/examples_pump_Stochastic_Modelling_28_0.png

As shown, even with all of these random states, it doesn’t appear that the behavior actually changes stochastically over time. This is because, by default, the model is set to run deterministically, meaning the stochastic states take their default values. To run stochastically, we use the option run_stochastic=True.

[15]:
mdl = Pump()
endclass, mdlhist = propagate.nominal(mdl, run_stochastic=True)
[16]:
fig, axs = mdlhist.plot_line('fxns.import_ee.r.s.effstate',
                            'fxns.import_ee.r.s.grid_noise',
                            'fxns.import_signal.r.s.sig_noise',
                            'fxns.move_water.r.s.eff')
../../_images/examples_pump_Stochastic_Modelling_31_0.png

As shown, this is more what we would expect from a random . Note that this simulation comes from the default model seed, and will this will always be the same. To get a different seed, we can pass new modelparams as an argument to propagate.nominal

[17]:
mdl = Pump()
endclass, mdlhist = propagate.nominal(mdl, run_stochastic=True,
                                      mdl_kwargs=dict(r={'seed':110}))
fig, axs = mdlhist.plot_line('fxns.import_ee.r.s.effstate',
                            'fxns.import_ee.r.s.grid_noise',
                            'fxns.import_signal.r.s.sig_noise',
                            'fxns.move_water.r.s.eff')
../../_images/examples_pump_Stochastic_Modelling_33_0.png

We can further simulate faults as we would before.

[18]:
mdl = Pump()
endclass, mdlhist = propagate.one_fault(mdl, 'export_water','block', time=20,
                                       run_stochastic=True, mdl_kwargs=dict(r={'seed':110}))
fig = mdlhist.plot_line('fxns.move_water.r.s.eff',
                        'fxns.move_water.s.total_flow',
                        'flows.wat_2.s.flowrate',
                        'flows.wat_2.s.pressure',
                        time_slice=[20], legend_loc=False)
../../_images/examples_pump_Stochastic_Modelling_35_0.png

As shown, the stochastic states still simulated over time after the fault.

One thing to watch in stochastic models is that off-nominal behavior may be more difficult to track, since stochastic behavior may vary without necessarily being adverse behavior. This can be a problem because many of the visualizations (e.g., in graph) are based on finding differences between faulty and nominal models to visualize degradation, these methods may be less useful/reliable.

[19]:
fig = mdlhist.plot_line('fxns.import_ee.r.s.effstate', 'fxns.import_ee.r.s.grid_noise',
                        'fxns.import_signal.r.s.sig_noise', 'fxns.move_water.r.s.eff', legend_loc=False)
../../_images/examples_pump_Stochastic_Modelling_37_0.png

In the blockage scenario, there is no effect on the underlying stochastic states, since nothing was set up for this in the behavior. In the no_sig fault, on the other hand, we defined the signal noise to go to zero, as shown:

[20]:
mdl = Pump()
endclass, mdlhist = propagate.one_fault(mdl, 'import_signal', 'no_sig', time=20,
                                        run_stochastic=True, mdl_kwargs=dict(r={'seed':110}))
fig = mdlhist.plot_line('fxns.import_ee.r.s.effstate',
                        'fxns.import_ee.r.s.grid_noise',
                        'fxns.import_signal.r.s.sig_noise',
                        'fxns.move_water.r.s.eff',
                         legend_loc=False,
                         time_slice=20)
../../_images/examples_pump_Stochastic_Modelling_39_0.png

Because stochastic models are non-deterministic, we are often interested not in the results of a single thread, but of the distribution of outcomes that might occur. To perform this kind of assessment, we can use a ParameterSample to instantiate the model with a number of different seeds.

[21]:
help(ParameterSample.add_variable_replicates)
Help on function add_variable_replicates in module fmdtools.sim.sample:

add_variable_replicates(self, x_combos, replicates=1, seed_comb='shared', name='var', weight=1.0)
    Add replicates for the given cominations of variables.

    Parameters
    ----------
    x_combos : list
        List of combinations of variable values. [x_1, x_2...]. If empty, the
        default values of x are used.
    replicates : int, optional
        Number of replicates to add of the variable. The default is 1.
    seed_comb : str, optional
        How to combine seeds ('shared' or 'independent'). 'shared' uses the same
        seed for each value while 'independent' uses different seeds over all
        variable values. The default is 'shared'.
    name : str, optional
        Name prefix for the set of replicates. The default is 'var'.
    weight : float, optional
        Total weight for the replicates (i.e., total probability to divide between
        samples). The default is 1.0.

    Examples
    --------
    >>> ex_ps = ParameterSample(expd)
    >>> ex_ps.add_variable_replicates([[1,1], [2,2]])
    >>> ex_ps
    ParameterSample of scenarios:
     - rep0_var_0
     - rep0_var_1
    >>> scen0 =ex_ps.scenarios()[0]
    >>> scen0.prob
    0.5
    >>> scen0.p['x']
    1
    >>> scen0.p['y']
    1
    >>> scen0.inputparams
    {0: 1, 1: 1}
    >>> scen1 =ex_ps.scenarios()[1]
    >>> scen1.prob
    0.5
    >>> scen1.p['x']
    2
    >>> scen1.p['y']
    2
    >>> scen1.inputparams
    {0: 2, 1: 2}

The replicates option can also be used in ParameterSample methods to simultaneously change the input parameters and seeds of the model. Below, we create an sample to simulate the model 100 times.

[22]:
ps = ParameterSample()
ps.add_variable_replicates([], replicates=100)
[23]:
endclasses, mdlhists = propagate.parameter_sample(mdl, ps, run_stochastic=True)
SCENARIOS COMPLETE: 100%|██████████| 100/100 [00:09<00:00, 10.31it/s]

To evaluate this behavior over time, we can then use History.plot_line, to plot the distribution of behaviors over time. History.plot_line has a number of different options for visualization of distributions. For example, below we plot model states as individual lines:

[24]:
fig, ax = mdlhists.plot_line('fxns.move_water.r.s.eff', 'fxns.move_water.s.total_flow',
                             'flows.wat_2.s.flowrate', 'flows.wat_2.s.pressure',
                             'fxns.import_ee.r.s.effstate', 'fxns.import_ee.r.s.grid_noise',
                             'flows.ee_1.s.voltage', 'flows.sig_1.s.power',
                             color='blue', comp_groups={}, alpha=0.1, legend_loc=False)
../../_images/examples_pump_Stochastic_Modelling_46_0.png

Or as percentiles:

[25]:
fig = mdlhist.plot_line('fxns.move_water.r.s.eff', 'fxns.move_water.s.total_flow',
                             'flows.wat_2.s.flowrate', 'flows.wat_2.s.pressure',
                             'fxns.import_ee.r.s.effstate', 'fxns.import_ee.r.s.grid_noise',
                             'flows.ee_1.s.voltage', 'flows.sig_1.s.power',
                        color='blue', comp_groups={}, aggregation='percentile')
../../_images/examples_pump_Stochastic_Modelling_48_0.png

Or as a mean with confidence interval:

[26]:
fig, ax = mdlhists.plot_line('fxns.move_water.r.s.eff', 'fxns.move_water.s.total_flow',
                             'flows.wat_2.s.flowrate', 'flows.wat_2.s.pressure',
                             'fxns.import_ee.r.s.effstate', 'fxns.import_ee.r.s.grid_noise',
                             'flows.ee_1.s.voltage', 'flows.sig_1.s.power',
                             color='blue', comp_groups={}, aggregation='mean_ci')
../../_images/examples_pump_Stochastic_Modelling_50_0.png

We can also compare stochastic output over a set of scenarios using plot.hist with the comp_groups parameter, which places the scenarios in different groups.

This can be done by first creating an ParameterDomain with the variable we wish to compare. The pump still has one real parameter–the fault delay.

[27]:
from pump_stochastic import PumpParam
pd = ParameterDomain(PumpParam)
pd.add_variable("delay")
pd
[27]:
ParameterDomain with:
 - variables: {'delay': (0, 100)}
 - constants: {}
 - parameter_initializer: PumpParam

This can then be sampled with a ParameterSample:

[28]:
ps = ParameterSample(pd)
ps.add_variable_replicates([[1]], replicates=100, name="delay1")
ps.add_variable_replicates([[10]], replicates=100, name="delay10")
ps
[28]:
ParameterSample of scenarios:
 - rep0_delay1_0
 - rep1_delay1_1
 - rep2_delay1_2
 - rep3_delay1_3
 - rep4_delay1_4
 - rep5_delay1_5
 - rep6_delay1_6
 - rep7_delay1_7
 - rep8_delay1_8
 - rep9_delay1_9
 - ... (200 total)

Since this delay only shows up in blockage fault modes, to compare the behaviors, we will first simulate it in a nested approach with only the blockage fault added.

[29]:
import multiprocessing as mp
[30]:
faultdomains = {"fd": (("fault", "export_water", "block"), {})}
faultsamples = {"fs": (("fault_phases", "fd"), {})}
endclasses, mdlhists, apps=propagate.nested_sample(mdl, ps, run_stochastic=True, faultdomains=faultdomains, faultsamples=faultsamples)
NESTED SCENARIOS COMPLETE:   0%|          | 0/200 [00:00<?, ?it/s]
Faults found during the nominal run {'move_water': ['mech_break']}
NESTED SCENARIOS COMPLETE:   0%|          | 1/200 [00:00<01:26,  2.30it/s]
Faults found during the nominal run {'move_water': ['mech_break']}
NESTED SCENARIOS COMPLETE:   2%|▏         | 3/200 [00:01<01:28,  2.22it/s]
Faults found during the nominal run {'move_water': ['mech_break']}
NESTED SCENARIOS COMPLETE:   2%|▏         | 4/200 [00:01<01:32,  2.11it/s]
Faults found during the nominal run {'move_water': ['mech_break']}
NESTED SCENARIOS COMPLETE:   2%|▎         | 5/200 [00:02<01:31,  2.13it/s]
Faults found during the nominal run {'move_water': ['mech_break']}
NESTED SCENARIOS COMPLETE:   4%|▎         | 7/200 [00:03<01:25,  2.26it/s]
Faults found during the nominal run {'move_water': ['mech_break']}
Faults found during the nominal run {'move_water': ['mech_break']}
NESTED SCENARIOS COMPLETE:   4%|▍         | 8/200 [00:03<01:25,  2.24it/s]
Faults found during the nominal run {'move_water': ['mech_break']}
NESTED SCENARIOS COMPLETE:   5%|▌         | 10/200 [00:04<01:25,  2.21it/s]
Faults found during the nominal run {'move_water': ['mech_break']}
NESTED SCENARIOS COMPLETE:   6%|▌         | 11/200 [00:04<01:25,  2.22it/s]
Faults found during the nominal run {'move_water': ['mech_break']}
NESTED SCENARIOS COMPLETE:   6%|▋         | 13/200 [00:05<01:24,  2.22it/s]
Faults found during the nominal run {'move_water': ['mech_break']}
NESTED SCENARIOS COMPLETE:   7%|▋         | 14/200 [00:06<01:21,  2.29it/s]
Faults found during the nominal run {'move_water': ['mech_break']}
NESTED SCENARIOS COMPLETE:   8%|▊         | 17/200 [00:07<01:22,  2.21it/s]
Faults found during the nominal run {'move_water': ['mech_break']}
NESTED SCENARIOS COMPLETE:  10%|▉         | 19/200 [00:08<01:20,  2.26it/s]
Faults found during the nominal run {'move_water': ['mech_break']}
Faults found during the nominal run {'move_water': ['mech_break']}
NESTED SCENARIOS COMPLETE:  10%|█         | 20/200 [00:08<01:19,  2.28it/s]
Faults found during the nominal run {'move_water': ['mech_break']}
NESTED SCENARIOS COMPLETE:  10%|█         | 21/200 [00:09<01:16,  2.34it/s]
Faults found during the nominal run {'move_water': ['mech_break']}
NESTED SCENARIOS COMPLETE:  11%|█         | 22/200 [00:09<01:17,  2.30it/s]
Faults found during the nominal run {'move_water': ['mech_break']}
NESTED SCENARIOS COMPLETE:  12%|█▏        | 23/200 [00:10<01:16,  2.33it/s]
Faults found during the nominal run {'move_water': ['mech_break']}
NESTED SCENARIOS COMPLETE:  12%|█▏        | 24/200 [00:10<01:15,  2.32it/s]
Faults found during the nominal run {'move_water': ['mech_break']}
NESTED SCENARIOS COMPLETE:  12%|█▎        | 25/200 [00:11<01:15,  2.32it/s]
Faults found during the nominal run {'move_water': ['mech_break']}
NESTED SCENARIOS COMPLETE:  14%|█▎        | 27/200 [00:11<01:14,  2.33it/s]
Faults found during the nominal run {'move_water': ['mech_break']}
NESTED SCENARIOS COMPLETE:  14%|█▍        | 29/200 [00:12<01:14,  2.30it/s]
Faults found during the nominal run {'move_water': ['mech_break']}
NESTED SCENARIOS COMPLETE:  15%|█▌        | 30/200 [00:13<01:13,  2.30it/s]
Faults found during the nominal run {'move_water': ['mech_break']}
NESTED SCENARIOS COMPLETE:  16%|█▌        | 31/200 [00:13<01:14,  2.27it/s]
Faults found during the nominal run {'move_water': ['mech_break']}
NESTED SCENARIOS COMPLETE:  16%|█▋        | 33/200 [00:14<01:15,  2.20it/s]
Faults found during the nominal run {'move_water': ['mech_break']}
NESTED SCENARIOS COMPLETE:  17%|█▋        | 34/200 [00:15<01:15,  2.20it/s]
Faults found during the nominal run {'move_water': ['mech_break']}
NESTED SCENARIOS COMPLETE:  18%|█▊        | 35/200 [00:15<01:15,  2.19it/s]
Faults found during the nominal run {'move_water': ['mech_break']}
NESTED SCENARIOS COMPLETE:  18%|█▊        | 36/200 [00:16<01:14,  2.21it/s]
Faults found during the nominal run {'move_water': ['mech_break']}
NESTED SCENARIOS COMPLETE:  19%|█▉        | 38/200 [00:16<01:13,  2.20it/s]
Faults found during the nominal run {'move_water': ['mech_break']}
NESTED SCENARIOS COMPLETE:  20%|█▉        | 39/200 [00:17<01:13,  2.19it/s]
Faults found during the nominal run {'move_water': ['mech_break']}
NESTED SCENARIOS COMPLETE:  20%|██        | 40/200 [00:17<01:13,  2.17it/s]
Faults found during the nominal run {'move_water': ['mech_break']}
NESTED SCENARIOS COMPLETE:  20%|██        | 41/200 [00:18<01:12,  2.19it/s]
Faults found during the nominal run {'move_water': ['mech_break']}
NESTED SCENARIOS COMPLETE:  22%|██▏       | 43/200 [00:19<01:11,  2.20it/s]
Faults found during the nominal run {'move_water': ['mech_break']}
NESTED SCENARIOS COMPLETE:  22%|██▏       | 44/200 [00:19<01:10,  2.20it/s]
Faults found during the nominal run {'move_water': ['mech_break']}
NESTED SCENARIOS COMPLETE:  23%|██▎       | 46/200 [00:20<01:09,  2.20it/s]
Faults found during the nominal run {'move_water': ['mech_break']}
NESTED SCENARIOS COMPLETE:  24%|██▎       | 47/200 [00:20<01:07,  2.28it/s]
Faults found during the nominal run {'move_water': ['mech_break']}
NESTED SCENARIOS COMPLETE:  24%|██▍       | 48/200 [00:21<01:06,  2.30it/s]
Faults found during the nominal run {'move_water': ['mech_break']}
NESTED SCENARIOS COMPLETE:  24%|██▍       | 49/200 [00:21<01:07,  2.24it/s]
Faults found during the nominal run {'move_water': ['mech_break']}
NESTED SCENARIOS COMPLETE:  27%|██▋       | 54/200 [00:24<01:07,  2.17it/s]
Faults found during the nominal run {'move_water': ['mech_break']}
NESTED SCENARIOS COMPLETE:  28%|██▊       | 55/200 [00:24<01:04,  2.24it/s]
Faults found during the nominal run {'move_water': ['mech_break']}
NESTED SCENARIOS COMPLETE:  28%|██▊       | 56/200 [00:24<01:03,  2.26it/s]
Faults found during the nominal run {'move_water': ['mech_break']}
NESTED SCENARIOS COMPLETE:  28%|██▊       | 57/200 [00:25<01:02,  2.30it/s]
Faults found during the nominal run {'move_water': ['mech_break']}
NESTED SCENARIOS COMPLETE:  29%|██▉       | 58/200 [00:25<01:00,  2.36it/s]
Faults found during the nominal run {'move_water': ['mech_break']}
NESTED SCENARIOS COMPLETE:  30%|███       | 60/200 [00:26<01:01,  2.29it/s]
Faults found during the nominal run {'move_water': ['mech_break']}
NESTED SCENARIOS COMPLETE:  30%|███       | 61/200 [00:27<01:00,  2.30it/s]
Faults found during the nominal run {'move_water': ['mech_break']}
NESTED SCENARIOS COMPLETE:  32%|███▏      | 63/200 [00:27<00:59,  2.29it/s]
Faults found during the nominal run {'move_water': ['mech_break']}
Faults found during the nominal run {'move_water': ['mech_break']}
NESTED SCENARIOS COMPLETE:  33%|███▎      | 66/200 [00:29<00:59,  2.24it/s]
Faults found during the nominal run {'move_water': ['mech_break']}
NESTED SCENARIOS COMPLETE:  34%|███▎      | 67/200 [00:29<00:58,  2.26it/s]
Faults found during the nominal run {'move_water': ['mech_break']}
NESTED SCENARIOS COMPLETE:  34%|███▍      | 68/200 [00:30<00:57,  2.31it/s]
Faults found during the nominal run {'move_water': ['mech_break']}
NESTED SCENARIOS COMPLETE:  34%|███▍      | 69/200 [00:30<00:57,  2.27it/s]
Faults found during the nominal run {'move_water': ['mech_break']}
NESTED SCENARIOS COMPLETE:  35%|███▌      | 70/200 [00:31<00:56,  2.29it/s]
Faults found during the nominal run {'move_water': ['mech_break']}
NESTED SCENARIOS COMPLETE:  36%|███▌      | 72/200 [00:32<00:58,  2.20it/s]
Faults found during the nominal run {'move_water': ['mech_break']}
NESTED SCENARIOS COMPLETE:  37%|███▋      | 74/200 [00:32<00:54,  2.31it/s]
Faults found during the nominal run {'move_water': ['mech_break']}
Faults found during the nominal run {'move_water': ['mech_break']}
NESTED SCENARIOS COMPLETE:  38%|███▊      | 76/200 [00:33<00:54,  2.28it/s]
Faults found during the nominal run {'move_water': ['mech_break']}
NESTED SCENARIOS COMPLETE:  39%|███▉      | 78/200 [00:34<00:53,  2.30it/s]
Faults found during the nominal run {'move_water': ['mech_break']}
Faults found during the nominal run {'move_water': ['mech_break']}
NESTED SCENARIOS COMPLETE:  40%|███▉      | 79/200 [00:35<00:52,  2.30it/s]
Faults found during the nominal run {'move_water': ['mech_break']}
NESTED SCENARIOS COMPLETE:  40%|████      | 80/200 [00:35<00:52,  2.29it/s]
Faults found during the nominal run {'move_water': ['mech_break']}
NESTED SCENARIOS COMPLETE:  40%|████      | 81/200 [00:35<00:52,  2.29it/s]
Faults found during the nominal run {'move_water': ['mech_break']}
NESTED SCENARIOS COMPLETE:  41%|████      | 82/200 [00:36<00:52,  2.25it/s]
Faults found during the nominal run {'move_water': ['mech_break']}
NESTED SCENARIOS COMPLETE:  42%|████▏     | 83/200 [00:36<00:52,  2.21it/s]
Faults found during the nominal run {'move_water': ['mech_break']}
NESTED SCENARIOS COMPLETE:  42%|████▏     | 84/200 [00:37<00:53,  2.17it/s]
Faults found during the nominal run {'move_water': ['mech_break']}
NESTED SCENARIOS COMPLETE:  43%|████▎     | 86/200 [00:38<00:52,  2.19it/s]
Faults found during the nominal run {'move_water': ['mech_break']}
Faults found during the nominal run {'move_water': ['mech_break']}
NESTED SCENARIOS COMPLETE:  44%|████▎     | 87/200 [00:38<00:50,  2.24it/s]
Faults found during the nominal run {'move_water': ['mech_break']}
NESTED SCENARIOS COMPLETE:  44%|████▍     | 88/200 [00:39<00:50,  2.22it/s]
Faults found during the nominal run {'move_water': ['mech_break']}
NESTED SCENARIOS COMPLETE:  46%|████▌     | 91/200 [00:40<00:48,  2.23it/s]
Faults found during the nominal run {'move_water': ['mech_break']}
NESTED SCENARIOS COMPLETE:  46%|████▋     | 93/200 [00:41<00:48,  2.21it/s]
Faults found during the nominal run {'move_water': ['mech_break']}
NESTED SCENARIOS COMPLETE:  47%|████▋     | 94/200 [00:41<00:46,  2.26it/s]
Faults found during the nominal run {'move_water': ['mech_break']}
NESTED SCENARIOS COMPLETE:  48%|████▊     | 97/200 [00:43<00:45,  2.25it/s]
Faults found during the nominal run {'move_water': ['mech_break']}
NESTED SCENARIOS COMPLETE:  50%|████▉     | 99/200 [00:43<00:43,  2.32it/s]
Faults found during the nominal run {'move_water': ['mech_break']}
Faults found during the nominal run {'move_water': ['mech_break']}
NESTED SCENARIOS COMPLETE:  78%|███████▊  | 156/200 [01:10<00:20,  2.18it/s]
Faults found during the nominal run {'import_ee': ['no_v']}
NESTED SCENARIOS COMPLETE:  86%|████████▋ | 173/200 [01:17<00:12,  2.11it/s]
Faults found during the nominal run {'import_ee': ['no_v']}
NESTED SCENARIOS COMPLETE: 100%|██████████| 200/200 [01:30<00:00,  2.21it/s]

Next, we get just the scenarios of interest:

[31]:
comp_mdlhists = mdlhists.get_scens('export_water_block_t27p0')

Finally, we create some comparison groups to group the results by:

[32]:
comp_groups = {'delay_1': ps.get_scens(p={'delay': 1}), 'delay_10': ps.get_scens(p={'delay': 10})}

These are the resulting behaviors:

[33]:
figax = comp_mdlhists.plot_line('fxns.move_water.r.s.eff', 'fxns.move_water.s.total_flow',
                             'flows.wat_2.s.flowrate', 'flows.wat_2.s.pressure',
                             'fxns.import_ee.r.s.effstate', 'fxns.import_ee.r.s.grid_noise',
                             'flows.ee_1.s.voltage', 'flows.sig_1.s.power',
                                comp_groups=comp_groups, aggregation='percentile', time_slice=27)
../../_images/examples_pump_Stochastic_Modelling_65_0.png

There a few interesting aspects of this simulation: - First, some of the delay_1 design simulations have a zero flowrate before the fault is injected. This is because the reduced delay means that nominal behavior can easily cause the fault–all that nees to happen is for the pressure to go over 15 for two time-steps, which is entirely forseeable with all the variability in the simulation. This can also be seen in the “total_flow” graph. - Second, the delay_10 gives the nominal behavior until the fault time, when pressure increases dramatically. This is also observed in the delay_1 design, but with a much smaller delay (1 instead of 1).

Finally, running multiple simulations relies on the NominalApproach, it can be combined fairly easily with existing methods used in conjunction with NominalApproach. (e.g., tabulate.NestedComparison).

[34]:
import numpy as np
from fmdtools.analyze.tabulate import NestedComparison
comp = NestedComparison(endclasses, ps, ["p.delay"], apps, ["fault"], metrics=['cost'], ci_metrics=['cost'], default_stat=np.mean)
[35]:
comp.as_table()
[35]:
cost cost_lb cost_ub
10 block 15151.128868 14469.853131 15852.714455
1 block 13380.052288 12920.289512 13880.426406
[36]:
comp.as_plot("cost", color_factor="fault")
[36]:
(<Figure size 600x400 with 1 Axes>, <Axes: xlabel='p.delay'>)
../../_images/examples_pump_Stochastic_Modelling_70_1.png