This is a static snapshot. Code + results from a real run, frozen for viewing.
This model was originally developed by Defence Research and Development Canada (DRDC) for Royal Canadian Air Force pilot workforce planning. The run shown here uses dummy data, not real operational figures.
- Original source (
pycomod): github.com/DRDC-RDDC/OS_PyCoMod - this demo uses a fork with minor changes to run on DCP.- To dispatch it yourself on DCP: github.com/dan-distributive/pycomod
In [2]:
# Requires node.js (https://nodejs.org/en/download)
# Requires dcp, numpy, pandas, matplotlib (pip install dcp numpy pandas matplotlib)
# Requires the local OS_PyCoMod_Events/pycomod package alongside this notebook
# Requires a DCP identity/wallet (dcp keystore, or set explicitly below)
from datetime import datetime
import json
import numpy as np
import pycomod as pcm
# IMPORT AND INIT DCP
import dcp
dcp.init(scheduler='https://scheduler.distributed.computer')
from dcp import identity
from dcp import wallet
# DCP API AND PAYMENT KEYS
if not identity.check():
identity.set("0x908ed06b96a3cdc2466375dcc19d6edd5c61149a2d9fda2da8506e1dc3623ad2")
wallet.add(wallet.get("default"))
In [3]:
# Initialization dictionary for the multi-fleet model
mf_init = {'run': {'t': [0], # sim start time
'date': ['2025-11-07'], # sim start date
'tunit': ['W'], # sim time unit
'dt': [1], # sim time step
'end': [52*30], # sim end time
'reps': [10]}, # sim reps for monte-carlo runs
'model': {'AC1': ['<model.AC1>'],
'AC2': ['<model.AC2>'],
'AC3': ['<model.AC3>'],
'AC4': ['<model.AC4>']},
'model.AC1': {'Trg': ['<model.AC1.Trg>'],
'OTU': ['<model.AC1.OTU>'],
'G_s': [6,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8], # OTU production
'O_s': [4,4,4,4,4,4,11,14,19,19,18,18,18,19,19,19,19], # NOP growth
'Ops': ['<model.AC1.Ops>'],
'P_s': [33,42,48,55,55,55,55,55,55,55,55,55,55,55,55,55,55], # flying positions
'Y_s': [3.5,4,4.5,5,5,5,5,5,5,5,5,5,5,5,5,5,5], # YFR in 1000 hrs
'g': [0.1502403846153846]},
'model.AC1.Trg': {'F': [0],
'd': [52],
's': [0.8],
'P_0': [0],
'P_1': [0],
'P_2': [0],
'P_3': [0],
'P_4': [0],
'P_5': [0],
'P_6': [0],
'P_7': [0]},
'model.AC1.OTU': {'F': [0],
'd': [26],
's': [1.0],
'P_0': [0],
'P_1': [0],
'P_2': [0],
'P_3': [0],
'P_4': [0],
'P_5': [0],
'P_6': [0],
'P_7': [0]},
'model.AC1.Ops': {'r_p': [1.9230769230769231],
'phi': [1],
'E': [10],
'N': [35],
'P': [28],
'R': [300],
'a_E': [0.0015384615384615385],
'a_N': [0.0015384615384615385],
'g': [0.09615384615384616],
'h': [0.019230769230769232],
'mu': [1],
'r_E': [3.8461538461538463],
'r_I': [3.8461538461538463],
'r_Y': [96.15384615384616],
'th': [2],
'I_0': [1.875],
'I_1': [1.875],
'I_2': [1.875],
'I_3': [1.875],
'I_4': [1.875],
'I_5': [1.875],
'I_6': [1.875],
'I_7': [1.875]},
'model.AC2': {'Trg': ['<model.AC2.Trg>'],
'OTU': ['<model.AC2.OTU>'],
'G_s': [6,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8],
'O_s': [4,4,4,4,4,4,11,14,19,19,18,18,18,19,19,19,19],
'Ops': ['<model.AC2.Ops>'],
'P_s': [33,42,48,55,55,55,55,55,55,55,55,55,55,55,55,55,55],
'Y_s': [3.5,4,4.5,5,5,5,5,5,5,5,5,5,5,5,5,5,5],
'g': [0.1502403846153846]},
'model.AC2.Trg': {'F': [0],
'd': [52],
's': [0.8],
'P_0': [0],
'P_1': [0],
'P_2': [0],
'P_3': [0],
'P_4': [0],
'P_5': [0],
'P_6': [0],
'P_7': [0]},
'model.AC2.OTU': {'F': [0],
'd': [26],
's': [1.0],
'P_0': [0],
'P_1': [0],
'P_2': [0],
'P_3': [0],
'P_4': [0],
'P_5': [0],
'P_6': [0],
'P_7': [0]},
'model.AC2.Ops': {'r_p': [1.9230769230769231],
'phi': [1],
'E': [10],
'N': [35],
'P': [28],
'R': [300],
'a_E': [0.0015384615384615385],
'a_N': [0.0015384615384615385],
'g': [0.09615384615384616],
'h': [0.019230769230769232],
'mu': [1],
'r_E': [3.8461538461538463],
'r_I': [3.8461538461538463],
'r_Y': [96.15384615384616],
'th': [2],
'I_0': [1.875],
'I_1': [1.875],
'I_2': [1.875],
'I_3': [1.875],
'I_4': [1.875],
'I_5': [1.875],
'I_6': [1.875],
'I_7': [1.875]},
'model.AC3': {'Trg': ['<model.AC3.Trg>'],
'OTU': ['<model.AC3.OTU>'],
'G_s': [6,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8],
'O_s': [4,4,4,4,4,4,11,14,19,19,18,18,18,19,19,19,19],
'Ops': ['<model.AC3.Ops>'],
'P_s': [33,42,48,55,55,55,55,55,55,55,55,55,55,55,55,55,55],
'Y_s': [3.5,4,4.5,5,5,5,5,5,5,5,5,5,5,5,5,5,5],
'g': [0.1502403846153846]},
'model.AC3.Trg': {'F': [0],
'd': [52],
's': [0.8],
'P_0': [0],
'P_1': [0],
'P_2': [0],
'P_3': [0],
'P_4': [0],
'P_5': [0],
'P_6': [0],
'P_7': [0]},
'model.AC3.OTU': {'F': [0],
'd': [26],
's': [1.0],
'P_0': [0],
'P_1': [0],
'P_2': [0],
'P_3': [0],
'P_4': [0],
'P_5': [0],
'P_6': [0],
'P_7': [0]},
'model.AC3.Ops': {'r_p': [1.9230769230769231],
'phi': [1],
'E': [10],
'N': [35],
'P': [28],
'R': [300],
'a_E': [0.0015384615384615385],
'a_N': [0.0015384615384615385],
'g': [0.09615384615384616],
'h': [0.019230769230769232],
'mu': [1],
'r_E': [3.8461538461538463],
'r_I': [3.8461538461538463],
'r_Y': [96.15384615384616],
'th': [2],
'I_0': [1.875],
'I_1': [1.875],
'I_2': [1.875],
'I_3': [1.875],
'I_4': [1.875],
'I_5': [1.875],
'I_6': [1.875],
'I_7': [1.875]},
'model.AC4': {'Trg': ['<model.AC4.Trg>'],
'OTU': ['<model.AC4.OTU>'],
'G_s': [6,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8],
'O_s': [4,4,4,4,4,4,11,14,19,19,18,18,18,19,19,19,19],
'Ops': ['<model.AC4.Ops>'],
'P_s': [33,42,48,55,55,55,55,55,55,55,55,55,55,55,55,55,55],
'Y_s': [3.5,4,4.5,5,5,5,5,5,5,5,5,5,5,5,5,5,5],
'g': [0.1502403846153846]},
'model.AC4.Trg': {'F': [0],
'd': [52],
's': [0.8],
'P_0': [0],
'P_1': [0],
'P_2': [0],
'P_3': [0],
'P_4': [0],
'P_5': [0],
'P_6': [0],
'P_7': [0]},
'model.AC4.OTU': {'F': [0],
'd': [26],
's': [1.0],
'P_0': [0],
'P_1': [0],
'P_2': [0],
'P_3': [0],
'P_4': [0],
'P_5': [0],
'P_6': [0],
'P_7': [0]},
'model.AC4.Ops': {'r_p': [1.9230769230769231],
'phi': [1],
'E': [10],
'N': [35],
'P': [28],
'R': [300],
'a_E': [0.0015384615384615385],
'a_N': [0.0015384615384615385],
'g': [0.09615384615384616],
'h': [0.019230769230769232],
'mu': [1],
'r_E': [3.8461538461538463],
'r_I': [3.8461538461538463],
'r_Y': [96.15384615384616],
'th': [2],
'I_0': [1.875],
'I_1': [1.875],
'I_2': [1.875],
'I_3': [1.875],
'I_4': [1.875],
'I_5': [1.875],
'I_6': [1.875],
'I_7': [1.875]}}
# DCP work function / pipeline
def build_and_run_model(rep, init, start_date, start_time, end, tunit, dt):
dcp.progress()
import numpy as np #type: ignore
import pycomod as pcm #type: ignore
# - Generalized multi-step delay with success rate and initial pop
class Delay(pcm.Model):
def build(self, steps=8, delay=15, success=0.8, pop=50):
# multi-stage chain of pools
P_s = [self.pool(pop/steps, pool_type='int', name='P_{z}'.format(z=i)) for i in range(steps)]
P = self.equation(lambda: sum(P_s)) # total pop
C = self.pool(external=True) # external pool for successful people
F = self.pool(0, pool_type='int') # pool for unsuccessful people
d = self.parameter(delay) # mean delay parameter
s = self.parameter(success) # success rate parameter
# inter-step flows
F_s = [0]*steps
for i in range(steps-1):
F_s[i] = self.flow(eval("lambda: P_s[{z}]*steps/d".format(z=i), locals()), src=P_s[i], dest=P_s[i+1])
F_c = self.flow(lambda: P_s[-1]*steps/d*s, src=P_s[-1], dest=C) # successful flow to ext pool
F_f = self.flow(lambda: P_s[-1]*steps/d*(1-s), src=P_s[-1], dest=F) #unsuccessful flow to failed pool
return locals()
# MEAD model with generalized multi-step inexperienced pool (u5 removed)
class MEAD_Ix(pcm.Model):
def build(self, steps=8, inexp=15, exp=10, nop=35, pos=28):
# time unit for parameters is 1 week
# POOLS
I_s = [self.pool(inexp/steps, name="I_{z}".format(z=i)) for i in range(steps)]
I = self.equation(lambda: sum(I_s)) # total inexp pop
E = self.pool(exp, pool_type='int') # exp pop
N = self.pool(nop, pool_type='int') # non-operational positions (NOP)
# PARAMETERS
P = self.parameter(pos) # flying positions
R = self.parameter(300) # required flying hours to upgrade
g = self.parameter(5/52) # intake rate per timestep
h = self.parameter(1/52) # other exp losses per timestep
a_E = self.parameter(0.08/52) # exp attrition rate per timestep
a_N = self.parameter(0.08/52) # NOP attrition rate per timestep
r_I = self.parameter(200/52) # max inexp flying hours per timestep
r_E = self.parameter(200/52) # max exp mentoring hours per timestep
r_p = self.parameter(100/52) # min exp flying hours per timestep to maintain proficiency
r_Y = self.parameter(5000/52) # max total flying hours per timestep
mu = self.parameter(1) # mentee-hour:mentor-hour ratio
th = self.parameter(2) # inexp yfr cost (including mentoring)
phi = self.parameter(1) # exp yfr cost
# EQUATIONS
u1 = self.equation(lambda: r_I*I/R) # mentee-limited
u2 = self.equation(lambda: r_E*mu*E/R) # mentor-limited
u3 = self.equation(lambda: max(0, r_E*mu*(P-I)/R)) # position-limited
u4 = self.equation(lambda: r_Y/th/R) # YFR-limited
u = self.equation(lambda: min(u1, u2, u3, u4)) # net upgrade rate to exp
u_s = self.equation(lambda: u*steps/I) # inexp step flows
# FLOWS
# disable this flow because intake is coming from OTU model
#F_g = self.flow(lambda: g, dest=I_s[0], discrete=True, stochastic=False) # OTU production flow
# experience accumulation and upgrade
F_s = [0]*steps
for i in range(steps-1):
F_s[i] = self.flow(eval("lambda: I_s[{z}]*u_s".format(z=i), locals()), src=I_s[i], dest=I_s[i+1])
F_u = self.flow(lambda: I_s[-1]*u_s, src=I_s[-1], dest=E)
F_h = self.flow(lambda: h, src=E) # other loss flow
F_aE = self.flow(lambda: E*a_E, src=E, stochastic=True) # exp attrition flow
F_aN = self.flow(lambda: N*a_N, src=N, stochastic=True) # NOP attrition flow
F_EN = self.flow(lambda: F_aN, src=E, dest=N) # posting to NOP flow replacing NOP attrition
# OTHER OUTPUTS
I_ss = self.equation(lambda: g*R/r_I) # inexp steady state
E_ss = self.equation(lambda: (g - h - a_N*N)/a_E) # exp steady state
E_cor = self.equation(lambda: E if I + E <= P else P - I) # corrected exp pop
N_cor = self.equation(lambda: N if I + E <= P else N + I + E - P) # corrected NOP pop
E_crit = self.equation(lambda: (h + a_N*N)/(r_E*mu/R - a_E)) # critical min exp pop
I_crit = self.equation(lambda: P - g*R/r_E/mu) # critical max inexp pop
IE = self.equation(lambda: I+E)
return locals()
# Fleet model consisting of training and OTU delays and a MEAD model of the ops squadron
class Fleet(pcm.Model):
def build(self):
# time unit is weeks
# submodels
Trg = self.submodel(Delay(steps=8, delay=52, success=0.8, pop=0))
OTU = self.submodel(Delay(steps=8, delay=26, success=1.0, pop=0))
Ops = self.submodel(MEAD_Ix(steps=8, inexp=15, exp=10, nop=35, pos=28))
# limit tracked output; must be set post-construction
Trg.set_output('P')
OTU.set_output('P')
Ops.set_output('I', 'E', 'N', 'P', 'I_ss', 'E_ss', 'IE')
# connect submodels
Trg.connect(C=OTU.P_s[0])
OTU.connect(C=Ops.I_s[0])
# intake to training
g = self.parameter(7.8125/52)
Fin = self.flow(lambda: g, dest=Trg.P_s[0])
# position schedule
P_s = self.parameter([0]) # flying positions
O_s = self.parameter([0]) # additional NOP on top of N
G_s = self.parameter([0]) # intake plan
Y_s = self.parameter([0]) # YFR
@self.process(start=self.wait_sim_start())
def pos():
for i in range(17):
Ops.P.set(P_s[i])
Ops.h.set(O_s[i]*Ops.a_N)
Ops.r_Y.set(Y_s[i]*1000/52)
g.set(G_s[i]/0.8/52)
Ops.g.set(G_s[i]/52)
yield self.wait_delay(52)
return locals()
# Multi-fleet model
class MultiFleet(pcm.Model):
def build(self):
AC1 = self.submodel(Fleet())
AC2 = self.submodel(Fleet())
AC3 = self.submodel(Fleet())
AC4 = self.submodel(Fleet())
# limit output; written out to avoid a loop-var leaking into locals()
AC1.set_output('Trg', 'OTU', 'Ops')
AC2.set_output('Trg', 'OTU', 'Ops')
AC3.set_output('Trg', 'OTU', 'Ops')
AC4.set_output('Trg', 'OTU', 'Ops')
return locals()
mf = MultiFleet()
mf._run(end, dt, tunit, start_time, start_date, init)
# drop redundant per-leaf 'dates' (derivable from 'times') to cut payload size
def strip_dates(d):
for v in d.values():
if isinstance(v, dict):
v.pop('dates', None)
strip_dates(v)
strip_dates(mf._output)
# mf.t/mf.date never accumulate real history in this pycomod version; borrow a
# tracked leaf's 'times' instead
if rep == 0:
x_times = np.array(mf._output['AC1']['Trg']['P']['times'])
# use day precision; float * timedelta64['W'] truncates fractional weeks
tunit_days = mf.tunit.init_value / np.timedelta64(1, 'D')
elapsed_days = np.round(x_times * tunit_days).astype('int64')
x_dates = mf.date.init_value + elapsed_days.astype('timedelta64[D]')
result = {
'_output': mf._output,
**({
'model_type': type(mf).__name__,
'x_times': x_times,
'x_dates': x_dates,
} if rep == 0 else {}),
}
# manually clear everything and force garbage collection
del mf
import gc
gc.collect()
return result
print("Model and pipeline ready")
Model and pipeline ready
In [4]:
# repitions to perform in the Monte Carlo simulation
reps = range(100)
# define DCP Job handle that maps build_and_run_model along with args to all the reps init, start_date, start_time, end, tunit, dt
run_start_date = '2025-11-07'
run_tunit = 'W'
# DCP JOB
job = dcp.compute_for(reps, build_and_run_model, [mf_init, run_start_date, 0, 52*30, run_tunit, 1])
# JOB CONFIG
job.public.name = '🪖 Multi-fleet'
job.fs.add('./OS_PyCoMod_Events/pycomod')
job.modules = ['numpy', 'pandas']
job.computeGroups = [{'joinKey':'demo', 'joinSecret':'dcp'}]
# EVENTS
job.on('readystatechange', lambda s: print(f"State: {s}"))
job.on('accepted', lambda _: print(f"\nJob ID: {job.id}Job accepted, awaiting results..."))
job.on('error', lambda e: print(json.dumps(e, indent=4).replace('\\n', '\n')))
job.on('result', lambda r: print(f"Computed result {int(r.sliceNumber)} of {len(reps)}"))
job.on('nofunds', lambda n: print(json.dumps(n, indent=4).replace('\\n', '\n')))
# EXEC
job.exec(0.01)
results = job.wait()
print('Done.')
State: exec
Passphrase prompt may be hidden because stdout is not tty.
Enter your passphrase (test123): ········
State: init State: preauth State: deploying State: listeners State: compute-groups State: uploading State: deployed Job ID: MpWnqP18I9PtHPJePvt2A0 Job accepted, awaiting results... Computed result 1 of 100 Computed result 2 of 100 Computed result 3 of 100 Computed result 4 of 100 Computed result 5 of 100 Computed result 6 of 100 Computed result 7 of 100 Computed result 8 of 100 Computed result 9 of 100 Computed result 10 of 100 Computed result 11 of 100 Computed result 12 of 100 Computed result 13 of 100 Computed result 14 of 100 Computed result 15 of 100 Computed result 16 of 100 Computed result 17 of 100 Computed result 18 of 100 Computed result 59 of 100 Computed result 19 of 100 Computed result 20 of 100 Computed result 60 of 100 Computed result 21 of 100 Computed result 22 of 100 Computed result 23 of 100 Computed result 24 of 100 Computed result 64 of 100 Computed result 25 of 100 Computed result 26 of 100 Computed result 27 of 100 Computed result 65 of 100 Computed result 28 of 100 Computed result 29 of 100 Computed result 30 of 100 Computed result 66 of 100 Computed result 31 of 100 Computed result 32 of 100 Computed result 67 of 100 Computed result 33 of 100 Computed result 34 of 100 Computed result 35 of 100 Computed result 36 of 100 Computed result 68 of 100 Computed result 37 of 100 Computed result 38 of 100 Computed result 69 of 100 Computed result 39 of 100 Computed result 40 of 100 Computed result 41 of 100 Computed result 42 of 100 Computed result 70 of 100 Computed result 43 of 100 Computed result 44 of 100 Computed result 71 of 100 Computed result 45 of 100 Computed result 46 of 100 Computed result 47 of 100 Computed result 72 of 100 Computed result 48 of 100 Computed result 49 of 100 Computed result 50 of 100 Computed result 73 of 100 Computed result 51 of 100 Computed result 52 of 100 Computed result 74 of 100 Computed result 54 of 100 Computed result 53 of 100 Computed result 55 of 100 Computed result 75 of 100 Computed result 56 of 100 Computed result 57 of 100 Computed result 58 of 100 Computed result 76 of 100 Computed result 61 of 100 Computed result 62 of 100 Computed result 63 of 100 Computed result 77 of 100 Computed result 78 of 100 Computed result 79 of 100 Computed result 80 of 100 Computed result 81 of 100 Computed result 82 of 100 Computed result 83 of 100 Computed result 84 of 100 Computed result 85 of 100 Computed result 86 of 100 Computed result 87 of 100 Computed result 88 of 100 Computed result 89 of 100 Computed result 90 of 100 Computed result 91 of 100 Computed result 92 of 100 Computed result 93 of 100 Computed result 94 of 100 Computed result 95 of 100 Computed result 96 of 100 Computed result 97 of 100 Computed result 98 of 100 Computed result 99 of 100 Computed result 100 of 100 Done.
In [5]:
# Show result data size if curious
import sys
import numpy as np
def total_size(obj, seen=None):
if seen is None:
seen = set()
obj_id = id(obj)
if obj_id in seen:
return 0
seen.add(obj_id)
if isinstance(obj, np.ndarray):
return obj.nbytes
size = sys.getsizeof(obj)
if isinstance(obj, dict):
size += sum(total_size(k, seen) + total_size(v, seen) for k, v in obj.items())
elif isinstance(obj, (list, tuple, set)):
size += sum(total_size(i, seen) for i in obj)
elif hasattr(obj, "__dict__"):
size += total_size(obj.__dict__, seen)
return size
# compute size
bytes_size = total_size(results)
mb_size = bytes_size / (1024 ** 2)
print("Size (MB):", mb_size)
Size (MB): 58.276193618774414
In [6]:
# Build _output_mc from DCP results, recursively.
# Works for any model structure.
def build_mc_output_generic(results):
# derive dates per leaf; elements update on different cadences
tunit_days = {'D': 1, 'W': 7}[run_tunit]
def dates_from_times(times):
elapsed_days = np.round(np.array(times) * tunit_days).astype('int64')
return np.datetime64(run_start_date) + elapsed_days.astype('timedelta64[D]')
def recurse(structure, path=()):
out = {}
for key, val in structure.items():
current_path = path + (key,)
if isinstance(val, dict) and 'values' in val:
# Leaf with values
mc_values = np.array([
get_from_path(r['_output'], current_path)['values']
for r in results
], dtype=float)
mc_times = val.get('times', results[0].get('x_times'))
mc_dates = dates_from_times(mc_times)
out[key] = {
'mc_values': mc_values,
'mc_times': mc_times,
'mc_dates': mc_dates
}
elif isinstance(val, dict):
# Recurse into nested dict
out[key] = recurse(val, path=current_path)
else:
# Plain numeric leaf
mc_values = np.array([
get_from_path(r['_output'], current_path)
for r in results
], dtype=float)
out[key] = {
'mc_values': mc_values,
'mc_times': results[0].get('x_times'),
'mc_dates': results[0].get('x_dates')
}
return out
def get_from_path(d, path):
"""Utility: traverse nested dict using a tuple path"""
for p in path:
d = d[p]
return d
# Top level: just recurse whatever is in results[0]['_output']
return recurse(results[0]['_output'])
_output_mc = build_mc_output_generic(results)
# Build run_data dict from _output_mc and DCP results
run_data = {
'output_mc': _output_mc,
'model': results[0]['model_type'],
'init_hash': hash("dcp_run"),
'timestamp': str(datetime.now()),
'reps': len(results),
'x_times': results[0]['x_times'],
'x_dates': results[0]['x_dates']
}
# made mgr dict just so the plot calls are identical to the example
mgr = {}
mgr[job.public.name] = run_data
# Plot
run = job.public.name # must match the mgr[job.public.name] = run_data key above
output = 'output_mc'
fleet = 'AC2'
plt = pcm.Plotter(title='Fleet Model', ylabel='Population', fontsize=14, ylimit=[0,100], xdates=True)
plt.plot(mgr[run][output][fleet]['Trg']['P'], color='blue', interval=50, label='Trg', step=True)
plt.plot(mgr[run][output][fleet]['OTU']['P'], color='cyan', interval=50, label='OTU', step=True)
plt.plot(mgr[run][output][fleet]['Ops']['I'], color='green', interval=50, label='Ops Inexp', step=True)
plt.plot(mgr[run][output][fleet]['Ops']['E'], color='red', interval=50, label='Ops Exp', step=True)
plt.plot(mgr[run][output][fleet]['Ops']['IE'], color='black', interval=50, label='Ops pilots', step=True)
plt.plot(mgr[run][output][fleet]['Ops']['N'], color='orange', interval=50, label='NOP', step=True)
plt.plot(mgr[run][output][fleet]['Ops']['P'], color='black', interval=50, linestyle='--', label='Ops positions', step=True)
plt.plot(mgr[run][output][fleet]['Ops']['I_ss'], color='green', interval=50, linestyle=':', label='Iss', step=True)
plt.plot(mgr[run][output][fleet]['Ops']['E_ss'], color='red', interval=50, linestyle=':', label='Ess', step=True)
plt.show()
In [ ]: