Open Source System Modeling with Python and Generative AI
Presenters
Dr. Raymond Madachy, rjmadach@nps.edu
Ryan Bell, ryan.bell@nps.edu
Instructions
This tutorial can be viewed and executed in the Colab notebook. It should be copied into a personal Google Drive acccount with the menu command File->Save a copy in Drive.
It is also available as a tutorial online manual. Examples can be copied and pasted into other desktop or cloud-based tools.
Notebook Links
Shortened URL: https://tinyurl.com/yc5vc4vz
Exercise Solutions URL: https://tinyurl.com/bdhaz8yj
Presentation
Introduction
Open source software has a long and rich history.
Open source advantages:
Free
Flexible
Collaborative
Transparent
Innovation
Python has become a preferred language for engineering due to ease of use and extensive open source libraries.
Recent developments include the Systems Engineering Library (se-lib) to support systems and software engineering processes.
Open Source Ecosystem
Tools / IDEs
Platforms for open source sharing
Libraries
Standards
Models
Systems Engineering Library (se-lib) Goals
Lower access barrier to system modeling with open source tool environment.
Provide integrated capabilities for systems modeling, analysis, and documentation
Be digital engineering compliant.
Compatibility with other modeling tools and libraries
Compatibility of everything on desktop and cloud platforms
se-lib Current Capabilities
SysML, UML textual notation and diagrams, other diagram types
Simulation including continuous systems modeling with system dynamics and discrete event modeling
System cost modeling
System reliability modeling
Systems engineering process and project management
se-lib General Usage Features
Use Python for modeling to intersperse model data, system analysis and documentation.
Model data can be inline code, read from or to external files.
Round-trip engineering to support rapid iterative development.
Automated workflows for model management, reconciliation and version control.
Use Case Models
Sequence Models
Discrete Event Modeling
Continuous Systems Modeling with System Dynamics
Graphical User Interface
Causal Analysis
Requirements Modeling
Project Modeling
Integrated Requirements, Effort and Staffing Models
Quantitative Fault Tree Analysis
Excel Data Import
se-lib Further Information
Install from PyPI with
pip install se-lib
Public repository at https://github.com/se-lib/se-lib/
Web site at http://se-lib.org
Python Introduction
Hello World
# print a string
print('Hello Systems Engineers at INCOSE')
# print a variable within a string
location = "Ottawa"
print(f'Hello Systems Engineers at INCOSE in {location}')
Python Operators
Arithmetic Operators
Arithmetic operators in Python are used to perform mathematical operations. They work with numeric data types such as integers and floating-point numbers.
Table of Arithmetic Operators
Operator |
Name |
Description |
Example |
---|---|---|---|
|
Addition |
Adds two operands |
|
|
Subtraction |
Subtracts the right operand from the left operand |
|
|
Multiplication |
Multiplies two operands |
|
|
Division |
Divides the left operand by the right operand |
|
|
Floor Division |
Divides and rounds down to the nearest integer |
|
|
Modulus |
Returns the remainder of the division |
|
|
Exponentiation |
Raises the left operand to the power of the right |
|
Usage Examples
a = 10
b = 3
print(a + b) # 13
print(a - b) # 7
print(a * b) # 30
print(a / b) # 3.3333...
print(a // b) # 3
print(a % b) # 1
print(a ** b) # 1000
Notes
The division operator (
/
) always returns a float in Python 3.x.Floor division (
//
) discards the fractional part, effectively rounding down.The modulus operator (
%
) is useful for determining if a number is even or odd.Be careful with the order of operations. Python follows the standard mathematical order (PEMDAS).
Logical Operators
Logical operators in Python are used to combine conditional statements. They allow you to create complex conditions by joining simpler ones. Python supports three main logical operators:
and
: Returns True if both operands are Trueor
: Returns True if at least one operand is Truenot
: Returns the opposite of the operand’s boolean value
Truth Table
Here’s a truth table showing the results of these operators:
A |
B |
A and B |
A or B |
not A |
---|---|---|---|---|
True |
True |
True |
True |
False |
True |
False |
False |
True |
False |
False |
True |
False |
True |
True |
False |
False |
False |
False |
True |
Usage Examples
x = True
y = False
print(x and y) # False
print(x or y) # True
print(not x) # False
print(not y) # True
Remember that Python uses short-circuit evaluation for and
and or
. This means it stops evaluating as soon as the result is determined. For and
, if the first operand is False, it doesn’t evaluate the second. For or
, if the first operand is True, it doesn’t evaluate the second.
Comparison Operators in Python
Comparison operators in Python are used to compare values. They return Boolean results (True or False) and are often used in conditional statements and loops.
Table of Comparison Operators
Operator |
Name |
Description |
Example |
---|---|---|---|
|
Equal to |
Returns True if both operands are equal |
|
|
Not equal to |
Returns True if operands are not equal |
|
|
Greater than |
Returns True if left operand is greater than the right |
|
|
Less than |
Returns True if left operand is less than the right |
|
|
Greater than or equal to |
Returns True if left operand is greater than or equal to the right |
|
|
Less than or equal to |
Returns True if left operand is less than or equal to the right |
|
Usage Examples
a = 5
b = 3
print(a == b) # False
print(a != b) # True
print(a > b) # True
print(a < b) # False
print(a >= b) # True
print(a <= b) # False
Notes
The equality operator (
==
) checks for value equality, not identity. For identity comparison, use theis
operator.Be careful not to confuse the assignment operator (
=
) with the equality comparison operator (==
).Comparison operators can also be used with strings, where the comparison is based on lexicographical order.
When comparing floating-point numbers, be aware of potential precision issues.
Chaining Comparisons
Python allows chaining of comparison operators:
x = 5
print(1 < x < 10) # True
print(10 < x < 20) # False
This is equivalent to 1 < x and x < 10
but more readable.
Lists
Lists are containers for sequences of values enclosed in square brackets [1, 2, 3]
. Lists can be used for simulation data input, internal model relationships, or generated as output for analysis.
# define a list and print it
delay_times = [2, 4, 5.6, 12.4, 22, 33.5]
print(delay_times)
# access a list element starting from element 0
print(delay_times[3])
# sum a list
sum(delay_times)
79.5
Conditionals
Conditional statements allow your program to make decisions based on certain conditions. Python uses if
, elif
(short for “else if”), and else
for creating conditional logic.
Basic Structure
if condition1:
# code to execute if condition1 is True
elif condition2:
# code to execute if condition1 is False and condition2 is True
else:
# code to execute if all above conditions are False
Explanation
if statement:
This is the starting point of a conditional block.
If the condition is True, the indented code below it executes.
If False, Python moves to the next condition (if any).
elif statement:
Short for “else if”.
Used to check multiple conditions.
You can have multiple elif statements.
Executed only if all previous conditions were False.
else statement:
Optional. Used at the end of a conditional block.
Executes if all previous conditions were False.
Does not require a condition.
age = 75
if age < 18:
print("You are a minor.")
elif age >= 18 and age < 65:
print("You are an adult.")
else:
print("You are a senior citizen.")
Iteration
Iteration is the repetition (looping) of program statements based on sets of elements to process or with conditional logic to stop the repetition. This is done with a while
loop or for
loop.
For Loops
The for
statement is used to iterate over the elements of a sequence (e.g., a string, list, dictionary, or tuple) or other iterable object per the loop syntax:
for variable in sequence:
# block of code to be executed for each element in sequence
The nested statements will be repeated for each item in the sequence.
# a list that can be iterated on
satellite_parts = ['frame', 'gimbal', 'gps antennae', 'gps receiver', 'stellar gyroscope']
# loop through list and assign each value to the variable 'name'
for part in satellite_parts:
print (part)
Generate List in a For Loop
# create a list, populate it over multiple runs and display it
import random
import matplotlib.pyplot as plt
# initialize an empty list
wind_velocities = []
# generate 10 random values
for run in range(100):
wind_velocity = random.uniform(10, 15)
# append last value to list
wind_velocities.append(wind_velocity)
print(wind_velocities)
# plot the output values
fig, ax = plt.subplots()
ax.plot(wind_velocities)
fig
--------------
--------------
While Loop
The while
statement is used for repeated execution as long as an expression is true. The “while loop” syntax is below where the indented code block is repeated.
while condition:
# block of code to be executed if the condition is true
time = 0
end_time = 12
timestep = 1
while time < end_time:
# block of code to be executed each time step
print(time)
time += timestep
# if then statement
number = float(input('What is a number between 0 - 10? '))
if number < 5:
print("Your number was less than 5")
else:
print("Your number was 5 or greater")
--------------
Dictionaries
Dictionaries are data structures that store key-value pairs. The syntax for a dictionary is enclosing a comma-separated list of key-value pairs in curly braces {}
. The key-value pairs are separated by colons, and each key-value pair is separated by a comma.
The mass characteristics for the parts of a satellite design can be populated as below.
satellite_part_masses = {'frame': 310, 'gimbal':491, 'gps antennae': 311, 'gps receiver': 480, 'stellar gyroscope': 52}
Elements in a dictionary can be accessed using square braces []
to designate the key. To access the value of a specific key in a dictionary, put the key name in brackets: part_masses['gimbal']
.
print('the gimbal mass is', part_masses['gimbal'], 'grams')
A dictionary can be iterated over. The total satellite mass can be computed with:
satellite_mass = 0
for mass in part_masses.values():
satellite_mass += mass
print(satellite_mass)
or
sum([mass for mass in part_masses.values())
se-lib Usage
Inputs
Dictionaries are used in se-lib to model path connection probabilities for discrete nodes. The connections
parameter takes a dictionary with key names for the downstream nodes and values for the path probabilities. The following indicates that 70% of cars get charged and 30% leave impatient.
add_source('incoming_cars',
entity_name="Car",
num_entities = 50,
connections={'charger': .7, 'impatient_cars': .3},
interarrival_time='np.random.exponential(5)')
Outputs
For discrete event models, the se-lib run-model
function returns nested dictionaries of detailed run data. The following receives two dictionaries into model_data
and entity_data
.
model_data, entity_data = run_model()
The model data can be accessed after a run:
print(model_data)
{'incoming_cars': {'type': 'source', 'entity_name': 'Car', 'num_entities': 5, 'connections': {'charger': 0.7, 'impatient_cars': 0.3}, 'interarrival_time': 'np.random.exponential(5)'}, 'charger': {'type': 'server', 'resource': <simpy.resources.resource.Resource object at 0x3182a80>, 'connections': {'payment': 1}, 'service_time': 'np.random.uniform(0, 16)', 'waiting_times': [0.0, 5.735769735466874, 11.036409140922178, 10.30570564297802], 'service_times': [6.38027468354249, 13.049474042273246, 1.698692377649424, 5.379300261467211]}, 'payment': {'type': 'delay', 'connections': {'served_cars': 1}, 'delay_time': 'np.random.uniform(1, 3)'}, 'served_cars': {'type': 'terminate', 'connections': {}}, 'impatient_cars': {'type': 'terminate', 'connections': {}}}
Specific data can be accessed using key names of model elements. A list of waiting times can be viewed with:
model_data['charger']['waiting_times']
[0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.024614443564587418, 0.0, 0.618677096181159, 0.0, 0.0, 0.0, ...]
The entity data is a nesting of dictionaries.
print(entity_data)
{1: {'arrival': 3.104240322565024, 'nodes': [('charger', 9.484515006107515), ('payment', 12.455480111434294), ('served_cars', 12.455480111434294)]}, 2: {'arrival': 3.7487452706406406, 'nodes': [('charger', 22.53398904838076), ('payment', 23.61771972604127), ('served_cars', 23.61771972604127)]}, 3: {'arrival': 11.497579907458583, 'nodes': [('charger', 24.232681426030183), ('payment', 26.801667711338496), ('served_cars', 26.801667711338496)]}, 4: {'arrival': 12.61479066527684, 'nodes': [('impatient_cars', 12.61479066527684)]}, 5: {'arrival': 13.926975783052162, 'nodes': [('charger', 29.611981687497394), ('payment', 31.311378396046038), ('served_cars', 31.311378396046038)]}}
The data for a specific entity for node traversal times can be accessed.
print(entity_data[3])
{'arrival': 11.497579907458583, 'nodes': [('charger', 24.232681426030183), ('payment', 26.801667711338496), ('served_cars', 26.801667711338496)]}
For more information and examples for discrete event modeling see http://se-lib.org/online/discrete_event_modeling_demo.html.
Random Number Examples
# see random number documentation at https://docs.python.org/3.9/library/random.html
# random.random() #Return floating point number in the range [0.0, 1.0).
# random.uniform(a, b) #Return a random floating point number uniformly distributed in the range (a, b)
# random.randint(a, b) #Return a random integer N such that a <= N <= b.
# random.normalvariate(mu, sigma) #Normal distribution. mu is the mean, and sigma is the standard deviation.
import random
#random number between 0 and 1
a=random.random()
print("random number = ", a)
#random number from uniform distribution
random_uniform = random.uniform(220, 500)
print("random uniform number = ", random_uniform)
#random integer between 100 and 200
random_integer = random.randint(100, 200)
print (f"random integer = {random_integer}")
#normal distribution variate with mean 100, standard deviation 15
random_normal = random.normalvariate(100, 15)
print(f"{random_normal = :6.2f}")
#print 10 random numbers in loop
print ("10 random numbers: ")
for run in range(10):
print(random.random()) #uniform number between 0 and 1
Importing Modules
To use the functionality of existing Python modules, also called libraries or packages, first load them into the interpreter using an import
or from
statement. The import
statement loads an entire module into the current namespace, which is a collection of names and their corresponding objects that can be accessed. The from
statement is used to import selected objects within a module.
The syntax for an import statement is:
import module_name
The following import loads the math module into the current namespace. It can then be used as a prefix to its functions. For example, the value of the pi constant can be accessed using the dot notation.
import math
print(math.pi)
Alternatively use the as keyword to define a different alias name.
import module_name as alias_name
As an example the random library will be imported and given the name rnd
with the as keyword. Its function to generate a random uniform number is called with the alias name using the dot notation.
import random as rnd
print(rnd.uniform(10, 20))
Frequently an alias name is defined as an abbreviation. For example, Numpy is usually imported as np
and all its functions and objects referred to with the np
name. It is highly recommended to stick with conventional names for overall compatibility and consistency.
The importing of frequently used general-purpose libraries are below. Shown are the conventional namespaces used for these libraries but they could be named anything that is non- conflicting. For SciPy, specific submodules are generally imported so there is no overall namespace used.
# general purpose library namespaces
# numpy for numerical computing with arrays
import numpy as np
# matplotlib for plotting and data visualization
import matplotlib.pyplot as plt
# pandas for data analysis
import pandas as pd
# scipy general purpose scientific computing statistics module
from scipy.stats import stats
The from
statement can be used to import specific objects from a module. It’s syntax is:
from module_name import object_name
This allows one to access the objects defined in a module without having to specify the full module name. For example, the following from statement imports the pi constant from the math module. Once it is imported, it can be accessed directly without having to write the math module prefix.
from math import pi
print(pi)
The import and from statements are similar in that they both load modules into the current namespace. While the import statement imports all of the objects in a module, the from statement imports specific objects from a module.
Functions
A Python function is a reusable named piece of code that can return value(s) or perform other operations with given input. Its syntax is:
def function_name(parameters separated by commas):
Optional docstring describing the function as a triple quoted
string
Set of statements including optional return() statement to send
back value(s)
A function can be called, or executed, by giving its name and any arguments it requires in parentheses. The arguments are matched up to expected input parameters. There must be the same number of calling arguments as defined parameters, and they are matched by position (except when using keywords). A function may or may not return value(s). If a function only prints output for example there is no explicit returned value. Multiple values can be returned and can be of different data types.
def sphere_volume(radius):
PI = 3.14159
volume = 4 / 3 * PI * radius ** 3
return(volume)
radius = 15
volume = sphere_volume(radius)
print(f'Volume of a sphere with radius {radius} is {volume:.1f}')
Objects
Python is an object oriented programming language. Classes define the name, atrributes, and functions of an object. Let’s consider a Car Class.
In this class, there are four attributes: make, model, year, and speed. Make, model, and year are set during object creation. Speed is created and set to 0 upon object creation.
We’ve also created two functions that will control the speed during simulation.
class Car:
def __init__(self, make, model, year):
self.make = make
self.model = model
self.year = year
self.speed = 0 # Initial speed is 0
def accelerate(self, increment):
"""Increase the speed of the car by the given increment."""
self.speed += increment
return f"The {self.year} {self.make} {self.model} is now going {self.speed} mph."
def brake(self, decrement):
"""Decrease the speed of the car by the given decrement, but not below 0."""
self.speed = max(0, self.speed - decrement)
return f"The {self.year} {self.make} {self.model} is now going {self.speed} mph."
# Example usage:
my_car = Car("Toyota", "Corolla", 2022)
print(my_car.accelerate(30))
print(my_car.accelerate(20))
print(my_car.brake(10))
print(my_car.brake(50)) # This will bring the speed to 0, not negative(my_car.accelerate(20))
class Car:
def __init__(self, make, model, year):
self.make = make
self.model = model
self.year = year
self.speed = 0 # Initial speed is 0
# Increase the speed of the car by the given increment.
def accelerate(self, increment):
self.speed += increment
return f"The {self.year} {self.make} {self.model} is now going {self.speed} mph."
# Decrease the speed of the car by the given decrement, but not below 0.
def brake(self, decrement):
"""Decrease the speed of the car by the given decrement"""
self.speed = max(0, self.speed - decrement)
return f"The {self.year} {self.make} {self.model} is now going {self.speed} mph."
# Example usage:
my_car = Car("Toyota", "Corolla", 2022)
print(my_car.accelerate(30))
print(my_car.accelerate(20))
print(my_car.brake(10))
print(my_car.brake(50)) # This will bring the speed to 0, not negative(my_car.accelerate(20))
List object attributes and methods
dir(my_car)
['__class__',
'__delattr__',
'__dict__',
'__dir__',
'__doc__',
'__eq__',
'__format__',
'__ge__',
'__getattribute__',
'__getstate__',
'__gt__',
'__hash__',
'__init__',
'__init_subclass__',
'__le__',
'__lt__',
'__module__',
'__ne__',
'__new__',
'__reduce__',
'__reduce_ex__',
'__repr__',
'__setattr__',
'__sizeof__',
'__str__',
'__subclasshook__',
'__weakref__',
'accelerate',
'brake',
'make',
'model',
'speed',
'year']
vars(my_car)
{'make': 'Toyota', 'model': 'Corolla', 'year': 2022, 'speed': 0}
Reading Input From The User
# get user inputs and make variable assignments
name = input('What is your first name? ')
last_name = input('What is your last name? ')
print ('Hello', name, last_name)
# get numeric user input, perform calculation and print result
# use float function to convert string input into number
number = float(input('What is the number to square? '))
number_squared = number * number
print(number, 'squared = ', number_squared)
Exception Handling
# add exception handler with try except blocks
try:
print(x)
except NameError:
print("Variable x is not defined")
except:
print("Something else went wrong")
# square number routine without exception handling
# prompt user for number and print its square
number = float(input('What is the number to square? ')) # converts string input to floating point number for calculations
number_squared = number * number
print(number, 'squared = ', number_squared)
# Trap invalid number input to avoid program crash and inform user.
# Continue loop until it is valid then break out of loop.
while True:
number = input('What is the number to square? ')
try:
number = float(number)
break # break out of the while loop
except ValueError:
print ("That is not a floating point number. Try again")
number_squared = number * number
print(number, 'squared = ', number_squared)
NumPy
NumPy (for Numerical Python) is the de-facto library for numerical computing with powerful tools for multi-dimensional arrays and matrices as essential data structures for engineering applications. NumPy arrays may resemble standard Python lists and they are used for some similar operations, but NumPy is optimized for fast, efficient computation on large datasets involving complex calculations. It is is written in C and compiled allowing for faster computation.
Beyond its core functionalities in handling arrays and matrices, it has a collection of mathematical functions to operate on those arrays, capabilities in probability and statistics for data analysis and scientific computing, and other advanced features. Importantly, NumPy integrates with other scientific libraries in the Python ecosystem, such as SciPy, Pandas, and Matplotlib, making it a powerful tool for data analysis and visualization. Many of them use NumPy internally.
NumPy’s primary object is the multidimensional array as a table of elements, all of the same type, indexed by a tuple of non-negative integers. Dimensions are called axes.
Task |
NumPy Functions and Operations |
---|---|
Array Creation and Attributes |
|
Create from list |
|
Set data type |
|
Access elements with slicing |
|
Get array shape |
|
Change data type |
|
Operations |
|
Element-wise operations |
|
Mathematical functions |
|
Linear algebra |
|
Array Manipulation |
|
Reshape arrays |
|
Concatenate arrays |
|
Split arrays |
|
Copy arrays |
|
Statistical Functions |
|
Mean, Median, etc. |
|
Minimum, Maximum |
|
Standard deviation |
|
Random Numbers |
|
Generate random numbers |
|
Generate random integers |
|
File Input and Output |
|
Save array |
|
Load array |
|
Linear Algebra |
|
Matrix multiplication |
|
Solving equations |
|
Eigenvalues and eigenvectors |
|
ARRAY CREATION AND BASIC OPERATIONS
After importing NumPy, arrays can be created from Python lists or tuples using the array function.
import numpy as np
a = np.array([1, 2, 3]) # Create a 1D array
b = np.array([[1, 2, 3], [4, 5, 6]]) # Create a 2D array (matrix)
NumPy arrays support a variety of operations that are performed element-wise and are faster than their standard Python counterparts. It can perform arithmetic operations like addition, subtraction, multiplication, and division on arrays.
c = a + b # Element-wise addition
d = a * b # Element-wise multiplication
print('addition\n', c, '\nmultiplication\n', d)
addition
[[2 4 6]
[5 7 9]]
multiplication
[[ 1 4 9]
[ 4 10 18]]
ARRAY FUNCTIONS
NumPy provides a vast library of mathematical functions that can operate on arrays. It also include extensive statistical functions, probability distributions, histograms and data binning, correlation and covariance, and various array data transformations.
Following are some examples in these areas. The next example creates arrays for projectile time and distance. It includes NumPy trigonometric functions, the linspace
function to create an array of evenly spaced discrete time points, and generates a distance array of the same length created by multiplication. These arrays will be used later for plotting trajectories vs. angle.
# create projectile time and distance arrays
import numpy as np
def flight_time(v0, angle_deg, g=9.81):
angle_rad = np.radians(angle_deg)
return 2 * v0* np.sin(angle_rad) / g
v0 = 100 # initial velocity (m/s)
angle = 45 # launch angle (degrees)
time_of_flight = flight_time(v0, angle)
time_points = np.linspace(0, time_of_flight, num=10) # time point array
distances = v0* np.cos(np.radians(angle)) * time_points # distance array
print(f'{time_points=} \n {distances=}')
time_points=array([ 0. , 1.60178227, 3.20356453, 4.8053468 , 6.40712906,
8.00891133, 9.61069359, 11.21247586, 12.81425813, 14.41604039])
distances=array([ 0. , 113.26311021, 226.52622041, 339.78933062,
453.05244082, 566.31555103, 679.57866123, 792.84177144,
906.10488164, 1019.36799185])
NumPy has basic statistical functions like mean, median, var, np.std, and more for summarizing and understanding data distributions. These can be applied to both NumPy arrays and standard lists.
The random
module includes methods to generate random numbers from statistical distributions which are useful in simulations, random sampling, and probability experiments. The following creates an array of 1000 values of a Rayleigh distribution to model wind velocity. The scale is the mean value and the size parameter is the number of samples.
np.random.rayleigh(scale=5, size=1000)
array([ 8.54395347, 6.29693514, 6.81646363, 6.59769644, 6.484059 ,
0.79192554, 3.91892555, 2.59195087, 9.20255815, 3.26062015,
1.04682111, 10.16864214, 6.76128808, 4.40085928, 8.34416138,
...
Task |
Examples |
---|---|
Arrays |
|
Create arrays |
|
* From lists: |
|
* With data type: |
|
Access elements |
|
Get array shape |
|
Change data type |
|
Operations |
|
Element-wise operations |
|
Mathematical functions |
|
Linear algebra |
|
Array Manipulation |
|
Reshape arrays |
|
Concatenate arrays |
|
Split arrays |
|
Copy arrays |
|
Statistical Functions |
|
Mean, Median, etc. |
|
Minimum, Maximum |
|
Standard deviation |
|
Random Numbers |
|
Generate random numbers |
|
Generate random integers |
|
Input & Output |
|
Save array |
|
Load array |
|
Linear Algebra |
|
Matrix multiplication |
|
Solving equations |
|
Eigenvalues & eigenvectors |
|
Numpy Random Number Distributions with Matplotlib Visualization
import numpy as np
import matplotlib.pyplot as plt
# Set the number of samples to generate for each distribution
number_samples = 100000
# Generate random numbers from different distributions using NumPy
uniform = np.random.uniform(2, 5, number_samples)
normal = np.random.standard_normal(number_samples)
triangle = np.random.triangular(0, 2, 8, number_samples)
rayleigh = np.random.rayleigh(scale=15, size=number_samples)
# Set the number of bins for the histograms
num_bins = 15
# Create a 2x2 plot with subplots for each distribution
figure, ((axis1, axis2), (axis3, axis4)) = plt.subplots(2, 2)
# Set the title of the figure
figure.suptitle('# samples = ' + str(number_samples))
# Define a kwargs dictionary with color and rwidth parameters to style histograms
kwargs = {'color': 'blue', 'rwidth': 0.9}
# Create histograms of the data with the kwargs applied
axis1.set(title='uniform', xlabel="Value", ylabel="Frequency")
axis1.hist(uniform, num_bins, **kwargs)
axis2.set(title='normal', xlabel="Value", ylabel="Frequency")
axis2.hist(normal, num_bins, **kwargs)
axis3.set(title='Triangle', xlabel="Value", ylabel="Frequency")
axis3.hist(triangle, num_bins, **kwargs)
axis4.set(title='Rayleigh', xlabel="Value", ylabel="Frequency")
axis4.hist(rayleigh, num_bins, **kwargs)
# Adjust the spacing between subplots
plt.subplots_adjust(hspace=0.6, wspace=0.4)
# Display the plot
# plt.show()
--------------
Matplotlib
Matplotlib is a fundamental library for creating static, animated, and interactive visualizations. It is widely used for data analysis and exploration in engineering and science providing many customization options for creating publication-quality graphics. These include line plots, scatter plots, bar plots, histograms, continuous distributions, many more, and combinations.
Plots can be specified with data input consisting of pairwise data, statistical distributions, grid data, 3D and volumetric data. It provides a range of backends for generating different graphic formats including vector or bitmapped static images, and animated videos. A full gallery of examples is at https://matplotlib.org/stable/gallery/index.html.
Central to Matplotlib is Pyplot, a comprehensive collection of functions for adding or customizing plot elements such as lines, axes, ticks, images, text, labels, decorations, etc as shown below. It is typically imported with import matplotlib.pyplot as plt
.
FUNDAMENTALS
Matplotlib has both an object-based Axes interface and a function-based pyplot interface. The Axes interface is used to create a Figure and one or more Axes objects, then use methods explicitly on the named objects to add data, configure limits, set labels etc. The pyplot interface consists of functions to manipulate Figure and Axes that are implicitly present (also called state-based). pyplot is mainly intended for interactive plots and simple cases of programmatic plot generation. Many functions are identical in both interfaces though the object-oriented API provides more flexibility overall.
Below contrasts the two interfaces to draw an identical line plot. Though an extra line is needed with the Axes interface to instantiate figure and axis objects, the pyplot interface will require more overhead if additional plots are created and incurs some limitations. Note that the end statement plt.show() isn’t necessary now in many environments to display the plots. A plt.savefig() function is used to save a graphic file.
Axes interface
The pyplot subplots method returns figure and axis objects with methods to add data, titles, etc. Figure methods manipulate the larger canvas attributes that axes reside in.
import matplotlib.pyplot as plt
fig, ax = plt.subplots()
ax.plot(x, y)
ax.set_title("Sample plot")
plt.show()
pyplot interface
import matplotlib.pyplot as plt
plt.plot(x, y)
plt.title("Sample plot")
plt.show()
The anatomy of a combined line and scatter plot shows the components for a figure and methods to manipulate them. The lines are drawn with ax.plot and scatter plot with ax.scatter . The axis labels can be set with the ax.set_xlabel
and ax.set_ylabel
methods respectively, and other components similarly.
temperatures = [303, 341, 315, 320, 301, 320, 330, 330, 323, 309, 310, 330, 333, 320, 310, 330, 323, 299, 310, 309, 293, 300, 310, 314]
# input y values
plt.plot(temperatures)
plt.xlabel('Hour')
plt.ylabel('Tank Temperature (K)')
plt.show()
--------------
import matplotlib.pyplot as plt
g = 9.81 # m/s^2
velocities = range(10, 101, 10)
heights = [velocity**2/g/2 for velocity in velocities]
# input x and y data lists
plt.plot(velocities, heights)
plt.xlabel('Velocity (m/s)')
plt.ylabel('Height (m)')
plt.show()
--------------
Random Number Scatter Plot
import matplotlib.pyplot as plt
import numpy as np
#x = np.random.randn(1000)
x = np.random.uniform(-3, 3, 1000)
y = np.random.uniform(-6, 1, 1000)
fig, ax = plt.subplots()
ax.scatter(x, y)
--------------
--------------
Pandas
Pandas is the “Python Data Analysis Library” that provides rapid capability to explore data in many formats and integrates with other libraries. It provides data structures for efficient data manipulation and analysis, as well as tools for data cleaning, merging, and reshaping.
It is widely used in engineering applications for data analysis, simulations, and machine learning. Its compatibility with other libraries such as NumPy, SciPy, scikit-learn and others can save many steps and enable more sophisticated analyses due to the richness of the underlying data structures.
Central to Pandas is the basic DataFrame object with rows and columns with additional metadata.
The following reads an Excel file and puts the file contents put into a DataFrame named df using the read_excel function
in a single line.
import pandas as pd
# create dataframe
df = pd.read_excel('air densities.xlsx')
print(df)
Altitude Density
0 0.0 1.230000e+00
1 2000 1.010000e+00
2 4000 8.190000e-01
3 6000 6.600000e-01
4 8000 5.260000e-01
.. ... ...
96 192000 3.600000e-10
97 194000 3.600000e-10
98 196000 3.600000e-10
99 198000 3.600000e-10
100 200000 2.500000e-10
[101 rows x 2 columns]
Task |
Pandas Code |
---|---|
File Operations |
|
Load CSV |
|
Load Excel (xlsx) |
|
Save CSV |
|
Save Excel (xlsx) |
|
Selection and Indexing |
|
Select column(s) |
|
Select rows by label |
|
Select rows by condition |
|
Get column by label |
|
Manipulation |
|
Add new column |
|
Remove column |
|
Add/Remove rows (append/drop) |
|
Rename columns |
|
Sort DataFrame |
|
See this example to read our tutorial registrant file.
SciPy
Scipy is a general purpose library for scientific computing. It provides a wide range of tools for numerical optimization interpolation, signal processing, linear algebra, and more. Scipy is widely used in engineering data analysis, signal processing simulations, and optimization.
# https://docs.scipy.org/doc/scipy/reference/generated/scipy.stats.chisquare.html
from scipy.stats import chisquare
alpha = .05 # significance level
# Test the null hypothesis that the categorical data has the given frequencies.
# Each value represents the data counts for a distribution bin.
# The sum of the observed and expected frequencies must be the same.
observed_frequencies = [18, 18, 15, 20, 20]
expected_frequencies = [16, 18, 20, 19, 18]
X2, p_value = chisquare(observed_frequencies, expected_frequencies)
print(f'{X2=} {p_value=}')
if p_value < alpha:
print('Reject the null hypothesis')
else:
print('Fail to reject the null hypothesis')
import pandas as pd
import matplotlib.pyplot as plt
import numpy as np
# Read the CSV file with no header, assigning names to the columns
temperature_data = pd.read_csv("temperature_data.csv")
# Calculate mean and standard deviation of temperature
temperature_mean = np.mean(temperature_data["Temperature"])
temperature_std = np.std(temperature_data["Temperature"]) # Sample standard deviation
print(f"Mean: {temperature_mean:.2f}\nStd Dev: {temperature_std:.2f}")
# Create a figure and an axes object
fig, ax = plt.subplots()
# Plot temperature vs time on the axes
ax.plot(temperature_data["Time"], temperature_data["Temperature"])
# Set labels for the axes
ax.set(xlabel ="Time", ylabel = "Temperature")
# Show the plot
plt.show()
Python Exercises
Let’s practice what we’ve learned.
Remember that just like systems engineering, there’s not always a single correct solution. While your solution may look different than what you see in the tutorial exercise solutions notebook, that does not mean your solutions are wrong.
Exercise 1: List of Random Numbers
Generate a list of 10 random numbers between 0 and 100. Store them in an variable called randNumbers.
# Your Code Here
input()
Exercise 2: Sum and Average
Calculate and print the sum and average of the intervals array.
intervals = [1, 4, 7, 5, 8, 2, 6]
# Your Code Here
Exercise 3: Delay Times
An array, arrival_times, measures the time between arrival of cars to a gas station in seconds for an hour. Print the average time and whether the average arrival time of cars is below or above 30 seconds.
arrival_times = [34, 59, 53, 33, 27, 56, 40, 56, 24, 45, 57, 27, 59, 36, 55, 38, 54, 33, 17, 15, 32, 33, 43, 21, 21, 20, 43, 42, 17, 26, 47,
32, 30, 30, 28, 24, 49, 52, 35, 22, 36, 58, 59, 41, 54, 22, 49, 34, 39, 55, 31, 24, 42, 19, 15, 22, 53, 56, 26, 35, 29, 24,
56, 38, 16, 29, 20, 16, 46, 29, 28, 41, 25, 38, 22, 48, 47, 15, 36, 36, 27, 47, 38, 57, 37, 29, 55, 51, 21, 44, 41, 43, 40,
20, 22, 31, 35, 39]
# Your Code Here
Exercise 4: For Loops
Write a Python program that prints the numbers from 1 to 30. For each number that is a multiple of 3, print “INCOSE” instead of the number, and for the multiples of 5, print “IS”. For numbers which are multiples of both 3 and 5, print “INCOSE IS”.
# Your Code Here
Exercise 5: While Loops
Write a Python program that asks the user to guess a number between 1 and 30. The program should keep prompting the user for guesses until they guess the correct number. After each incorrect guess, the program should tell the user whether their guess was too high or too low.
# Your Code Here
se-lib Examples
install se-lib
!pip install se-lib
Causal Diagram
import selib as se
# causal relationships
relationships = [('Available personnel',"Workforce gap", "-"),
('Required personnel',"Workforce gap", "+"),
("Workforce gap", "Hiring rate", "+"),
("Hiring rate", "Available personnel", "+"),
]
# draw diagram
se.causal_diagram(relationships)
Critical Path Analysis
import selib as se
# tasks, durations and dependencies
tasks = [('A', {'Duration': 3}),
('B', {'Duration': 5}),
('C', {'Duration': 2}),
('D', {'Duration': 3}),
('E', {'Duration': 5})]
task_dependencies = [('A', 'C'),
('B', 'C'),
('A', 'D'),
('C', 'E'),
('D', 'E')]
# create diagram
se.critical_path_diagram(tasks, task_dependencies, filename="critical_path")
Quantitative Fault Tree
# UUV computed fault tree given probabilities for basic events
uuv_fault_tree = [
("UUV Mission Data Loss", "or", '', ["Communication Loss", "Power Down", "All Sensors Fail"]),
('All Sensors Fail', 'and', '', ['Low Resolution Sensor 1 Fails', 'Low Resolution Sensor 2 Fails', 'High Resolution Sensor 3 Fails']),
('Power Down', 'and', '', ["Main Power Down", "Backup Power Down"]),
('Communication Loss', 'basic', .003),
('Main Power Down', 'basic', .02),
('Backup Power Down', 'basic', .08),
('Low Resolution Sensor 1 Fails', 'basic', .001),
('Low Resolution Sensor 2 Fails', 'basic', .001),
('High Resolution Sensor 3 Fails', 'basic', .003),
]
se.draw_fault_tree_diagram_quantitative(uuv_fault_tree, filename="uuv_quantitative_fault_tree", format="svg")
Sequence Diagram
import selib as se
# system model
system_name = "Battle Simulator"
actors = ['Battle Planner']
objects = ['main']
actions = [
('Battle Planner', 'main', 'run()'),
('main', 'Battle Planner', 'request for side 1 name'),
('Battle Planner', 'main', 'side 1 name'),
('main', 'Battle Planner', 'request for side 2 name'),
('Battle Planner', 'main', 'side 2 name'),
('main', 'Battle Planner', 'request for side 1 starting level'),
('Battle Planner', 'main', 'side 1 starting level'),
('main', 'Battle Planner', 'request for side 1 lethality coefficient'),
('Battle Planner', 'main', 'side 1 lethality coefficient'),
('main', 'Battle Planner', 'request for side 2 starting level'),
('Battle Planner', 'main', 'side 2 starting level'),
('main', 'Battle Planner', 'request for side 2 lethality coefficient'),
('Battle Planner', 'main', 'side 2 lethality coefficient'),
('main', 'Battle Planner', 'time history of troops and victor'),
]
# create diagram
se.sequence_diagram(system_name, actors, objects, actions, filename=system_name+"_sequence_diagram")
System Dynamics Model
System dynamics function references
See more se-lib system dynamics examples on Google Colab.
import selib as se
# Battle Simulator using Lanchester's Law for Aimed Fire with Reinforcements
se.init_sd_model(start=0, stop=2, dt=.05)
step_size = 600
se.add_stock("blue_troops", 1000, inflows=["blue_reinforcements"], outflows=["blue_attrition"])
se.add_flow("blue_attrition", "red_troops*red_lethality", inputs=["red_troops", "red_lethality"])
se.add_flow("blue_reinforcements", "step(200, .7)-step(200, 1.2)", inputs=[])
se.add_auxiliary("blue_lethality", "random.uniform(.7, .9)")
se.add_stock("red_troops", 800, inflows=["red_reinforcements"], outflows=["red_attrition"])
se.add_flow("red_attrition", "blue_troops*blue_lethality", inputs=["blue_troops", "blue_lethality"])
se.add_flow("red_reinforcements", f"step({step_size}, .3)-step({step_size}, .7)")
se.add_auxiliary("red_lethality", "random.uniform(.8, 1.0)")
se.run_model()
se.save_graph(['blue_troops', 'red_troops'], filename="battle_simulator_troop_levels")
se.save_graph(["red_attrition", "red_reinforcements", 'blue_attrition', 'blue_reinforcements'], filename="battle_simulator_flow_rates")
se.draw_model_diagram()
Discrete Event Model
Discrete event modeling functions
init_de_model()
Instantiates a discrete event model for simulation
add_source(name, connections, num_entities, interarrival_time)
Add a source node to a discrete event model to generate entities.
Parameters: - name (string) – A name for the the source. - connections (dictionary) – A dictionary of the node connections after the source. The node names are the keys and the values are the relative probabilities of traversing the connections. - num_entities (integer) – Number of entities to be generated. - interarrival_time (float) – The time between entity arrivals into the system. May be a constant or random function.
add_server(name, connections, service_time, capacity=1)
Add a server to a discrete event model.
Parameters: - name (string) – A name for the server. - connections (dictionary) – A dictionary of the node connections after the server. The node names are the keys and the values are the relative probabilities of traversing the connection. - capacity (integer) – The number of resource usage slots in the server
add_delay(name, connections, delay_time)
Add a delay to a discrete event model.
Parameters: - name (string) – A name for the delay. - connections (dictionary) – A dictionary of the node connections after the delay. The node names are the keys and the values are the relative probabilities of traversing the connections. - delay_time (float) – The time delay for entities to traverse. May be a constant or random function.
The connections
parameter is a dictionary with the paths as keys for the probabilities of traversing the paths. The following illustrates a 70% probability of hit targets:
add_server(name='shooter',
connections={'missed_targets': .3, 'hit_targets': .7},
service_time='random.uniform(0, 16)',
capacity = 2)
add_terminate(name)
Add a terminate node to a discrete event model for entities leaving the system.
Parameters: - name (string) – A name for the terminate.
run_model(verbose=True)
Executes the current model
Returns: - If continuous, returns 1) Pandas dataframe containing run outputs for each variable each timestep and 2) model dictionary. - If discrete, returns 1) model dictionary with run statistics and 2) entity run data
draw_model_diagram()
Draw a diagram of the current model. File saving is not available in this online mode.
Returns: Rendered result in default viewing application. se-lib calls the Graphviz API for this. Return type: graph object view
General Notes
A source node for entities is required for a discrete event simulation.
The model data is available in the returned model dictionary for model structure and statistics, and entity data in its returned dictionary.
These could be accessed as follows after a simulation run:
model_data, entity_data = run_model()
print(model_data)
{'incoming_cars': {'type': 'source', 'connections': {'charger': 1}, 'interarrival_times': array([3.0309067 , 4.10624419, 1.02721424, 4.80644342, 1.74633087, 1.75962277, 3.2402905 , 3.01723568, 1.34778789, 3.00462931, 4.44005026, 3.7270276 , 2.85692434, 4.06819259, 3.58149013])}, 'charger': {'type': 'server', 'resource': , 'connections': {'exiting_cars': 1}, 'service_time': 'random.uniform(0, 16)', 'waiting_times': [0.0, 0.0, 3.4088079186861844, 5.55427241334754, 9.607255415852451, 9.377498559467139, 12.491879094760815, 10.315210789464626, 11.858530255400716, 13.825661415379944, 14.294341380548126, 18.585223211289723, 20.462802946330427, 17.862113499883627, 18.008486582943377]}, 'exiting_cars': {'type': 'terminate', 'connections': {}}}
Statistics are contained in the returned model dictionary. E.g., the waiting times for a server queue can be accessed as:
model_data['shooter']['waiting_times']
[0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.024614443564587418, 0.0, 0.618677096181159, 0.0, 0.0, 0.0, 0.19937965933672785, 0.47048625473610173]
import selib as se
# electric car charging simulation
se.init_de_model()
se.add_source('incoming_cars',
entity_name="Car",
num_entities = 10,
connections={'charger': .7, 'impatient_cars': .3},
interarrival_time='np.random.exponential(5)')
se.add_server(name='charger',
connections={'payment': 1},
service_time='np.random.uniform(0, 16)',
capacity = 1)
se.add_delay(name='payment',
delay_time = 'np.random.uniform(1, 3)',
connections={'served_cars': 1},)
se.add_terminate('served_cars')
se.add_terminate('impatient_cars')
se.draw_model_diagram()
model_data, entity_data = se.run_model()
se.plot_histogram(model_data['charger']['waiting_times'], xlabel="Charger Waiting Time")
Fault Tree
# UUV computed fault tree given probabilities for basic events
uuv_fault_tree = [
("UUV Mission Data Loss", "or", '', ["Communication Loss", "Power Down", "All Sensors Fail"]),
('All Sensors Fail', 'and', '', ['Low Resolution Sensor 1 Fails', 'Low Resolution Sensor 2 Fails', 'High Resolution Sensor 3 Fails']),
('Power Down', 'and', '', ["Main Power Down", "Backup Power Down"]),
('Communication Loss', 'basic', .003),
('Main Power Down', 'basic', .02),
('Backup Power Down', 'basic', .08),
('Low Resolution Sensor 1 Fails', 'basic', .001),
('Low Resolution Sensor 2 Fails', 'basic', .001),
('High Resolution Sensor 3 Fails', 'basic', .003),
]
se.draw_fault_tree_diagram_quantitative(uuv_fault_tree, filename="uuv_quantitative_fault_tree", format="svg")
Use Case Model
import selib as se
# system model
system_name = "Course Portal"
actors = ['Student', 'Instructor']
use_cases = ['Post Discussion', 'Take Quiz', 'Create Quiz']
interactions = [('Student', 'Post Discussion'), ('Instructor', 'Post Discussion'), ('Student', 'Take Quiz'), ('Instructor', 'Create Quiz')]
use_case_relationships = []
# create diagram
se.use_case_diagram(system_name, actors, use_cases, interactions, use_case_relationships, filename=system_name+'use case diagram.pdf')
Sequence Model
import selib as se
# system model
system_name = "Battle Simulator"
actors = ['Battle Planner']
objects = ['main']
actions = [
('Battle Planner', 'main', 'run()'),
('main', 'Battle Planner', 'request for side 1 name'),
('Battle Planner', 'main', 'side 1 name'),
('main', 'Battle Planner', 'request for side 2 name'),
('Battle Planner', 'main', 'side 2 name'),
('main', 'Battle Planner', 'request for side 1 starting level'),
('Battle Planner', 'main', 'side 1 starting level'),
('main', 'Battle Planner', 'request for side 1 lethality coefficient'),
('Battle Planner', 'main', 'side 1 lethality coefficient'),
('main', 'Battle Planner', 'request for side 2 starting level'),
('Battle Planner', 'main', 'side 2 starting level'),
('main', 'Battle Planner', 'request for side 2 lethality coefficient'),
('Battle Planner', 'main', 'side 2 lethality coefficient'),
('main', 'Battle Planner', 'time history of troops and victor'),
]
# create diagram
se.sequence_diagram(system_name, actors, objects, actions, filename=system_name+"_sequence_diagram")
Context Model
import selib as se
# system model
system_name = "Python Interpreter with PyML"
external_actors = [("User", "🧍"), "OS", "Graphviz"]
# create context diagram
se.context_diagram(system_name, external_actors, filename="pyml_context_diagram_offline")
Requirements Model
import selib as se
requirements = [("ISR", "Performance"),
("Performance", ("The UUV shall be capable of completing a mission of 6 hours duration.",
"The UUV shall be capable of a top speed of 14 knots.",
"The UUV shall be capable of surviving in an open ocean environment to a depth of 1500 meters.",
"The UUV shall avoid detection.")),
("ISR", "Communication"),
("Communication", ("R1.1.1 Mission parameters shall be uploadable to the UUV",
"R1.1.2 The UUV shall receive remote commands",
"R1.1.3 The UUV shall commence its mission when commanded",
"R1.1.4 The UUV shall be capable of transmitting data in a host ship compatible format",
"R1.1.5 The UUV shall indicate that it is ready for recovery")),]
se.tree(requirements, rankdir='LR', filename="uuv_requirements_tree")
Read Fault Tree from Excel
First copy the demonstration Excel file from Github by running the next cell.
!wget https://raw.githubusercontent.com/se-lib/se-lib/main/docs/excel_files/aav_fault_tree.xlsx
import selib as se
# read fault tree from Excel file into list of nodes
fault_tree_list = se.read_fault_tree_excel('aav_fault_tree.xlsx')
# create fault tree diagram
se.fault_tree_diagram(fault_tree_list)
Design Structure Matrix
import selib as se
tasks = ['Make Board', 'Acquire Wheels', 'Assemble', 'Test']
task_dependencies = [('Make Board', 'Assemble'), ('Acquire Wheels', 'Assemble'), ('Assemble', 'Test'), ('Test', 'Assemble')]
se.design_structure_matrix(tasks, task_dependencies, filename="skateboard_task_dsm_with_feedback")
Work Breakdown Structure
import selib as se
# project work breakdown structure
wbs_decompositions = [
('Skateboard', 'Hardware'), ('Skateboard', 'Software'), ('Skateboard', 'Integration and Test'),
('Hardware', 'Board'), ('Hardware', 'Wheels'), ('Hardware', 'Mounting'),
('Software', 'OS'), ('Software', 'GPS Driver'), ('Software', 'Route Tracking'),
('Integration and Test', 'Fixed Platform'), ('Integration and Test', 'Street Testing')]
# create diagram
se.tree(wbs_decompositions, filename="skateboard_wbs")
Extended Example: Target Shooter System
https://colab.research.google.com/drive/1jTyGvTt-bzmtNt_rhISm7gD36PtvxXQ0?usp=sharing
se-lib Exercises
Exercise 1: Water Pump Fault Trees
Draw a fault tree for a home well that utilizes a water pump.
The top event is “No Water.”
The first intermediate event is “Pump Failure”. Pump failure may be caused by “Mechanical Failure” (basic event) or failure of primary and backup electrical power sources.
The second intermediate event is “Water Line Blockage”. The basic events for “Water Line Blockage” are “Collapsed Pipe” or “Debris clogged Pipe”.
The probability of the basic events are below:
Mechanical Failure: 0.0005
Primary Power Failure: 0.003
Backup Power Failure: 0.003
Collapsed Pipe: 0.001
Debris Clogged Pipe: 0.002
# Your Code Here
Exercise 2: Bank ATM Sequence Diagram
Draw a sequence diagram that describes the interaction between a bank’s customer, an ATM, and the bank server. In this interaction, the transaction selected is ‘Request Balance’.
This diagram can get very long and complicated if modeling all the possible scenarios. For the purpose of this exercise, simplify the sequence diagram by assuming the PIN number is valid and the customer wants to do no more transactions.
# Your Code Here
Exercise 3: Course Portal Context Diagram
HINT: There are many solutions to this problem.
import selib as se
# system model
system_name = "Course Portal"
external_actors = ["Student"]
# create context diagram
se.context_diagram(system_name, external_actors)
Exercise 4: Electric Car Charging Simulation
In the electric car simulation below, modify the number of charging stations to 2, and then 3. How does this affect the charger wait time?
import selib as se
# electric car charging simulation
se.init_de_model()
se.add_source('incoming_cars',
entity_name="Car",
num_entities = 50,
connections={'charger': .7, 'impatient_cars': .3},
interarrival_time='np.random.exponential(5)')
se.add_server(name='charger',
connections={'payment': 1},
service_time='np.random.uniform(0, 16)',
capacity = 1)
se.add_delay(name='payment',
delay_time = 'np.random.uniform(1, 3)',
connections={'served_cars': 1},)
se.add_terminate('served_cars')
se.add_terminate('impatient_cars')
# draw the model diagram
se.draw_model_diagram()
# run the model
model_data, entity_data = se.run_model()
se.plot_histogram(model_data['charger']['waiting_times'], xlabel="Charger Waiting Time")
Exercise 5: Ship Dry Dock Utilization
A ship maintenance yard currently has one dry dock. When the shipyard was built, it was expected that it would service up to 8 ships a year. However, due to other ship yard closures, the shipyard is scheduled to receive 12 ships a year.
Incoming ships arrive every 25 to 35 days (uniformly distributed). Depending on the amount of maintenance required, ships spend 15 to 40 days in dry dock (uniformly distributed).
Time spent waiting for a dry dock is time a ship is unproductive and unable to produce revenue. However, up to 10 days of waiting for a dry dock is acceptable to give the crew a break.
Hint 1: Use a discrete event model.
Hint 2: A Monte Carlo simulation over hundreds or thousands of runs would provide a better estimate. However for this tutorial, a single run is good enough. You can continue your Python and se-lib journey by completing the Monte Carlo simulation at a later time.
# Your Code Here
# Your Code Here
--------------
SysML v2 Parts as Python Objects
A vehicles axles are defined by the mass and length as shown in the code snippet below.
part frontAxleAssembly:AxleAssembly{
attribute mass=800[kg];
attribute length=1.82[m];
}
part rearAxleAssembly:AxleAssembly{
attribute mass=750[kg];
attribute length=1.82[m];
}
If we wanted to use these values in Python, we could turn them into objects. Note that we are manually creating the objects below for this tutorial, but the SysML v2 code can also be parsed using a variety of Python functions to automatically create the objects.
class AxleAssembly:
def __init__(self, mass, length):
self.mass = mass
self.length = length
self.mass_unit = 'kg'
self.length_unit = 'm'
# Create an object of the AxleAssembly class
frontAxleAssembly = AxleAssembly(mass=800, length = 1.82)
rearAxleAssembly = AxleAssembly(750, 1.82)
# Print the mass attribute to verify
print(f"The mass of the front axle assembly is {frontAxleAssembly.mass} {frontAxleAssembly.mass_unit}")
print(f"The mass of the rear axle assembly is {rearAxleAssembly.mass} {rearAxleAssembly.mass_unit}")
# Calculate the combined mass of the axles
combinedMass = frontAxleAssembly.mass + rearAxleAssembly.mass
print(f"The combined mass of the axles is {combinedMass} {frontAxleAssembly.mass_unit}")
Converting Python Objects to SysML v2
The below function will convert a Python object to a SysML v2 Part.
def sysml2_part(name: str, obj: object) -> str:
class_name = obj.__class__.__name__
attributes = []
for attr, value in obj.__dict__.items():
if attr.endswith('_unit'):
continue
unit = getattr(obj, f"{attr}_unit", None)
unit_str = f"[{unit}]" if unit else ""
attributes.append(f" attribute {attr}={value}{unit_str};")
attributes_str = "\n".join(attributes)
sysml_template = f"""part {name}:{class_name}{{
{attributes_str}
}}"""
return sysml_template
Let’s try it with the axles we created above.
# Example usage:
front_sysml = sysml2_part("frontAxleAssembly", frontAxleAssembly)
rear_sysml = sysml2_part("rearAxleAssembly", rearAxleAssembly)
print(front_sysml)
print()
print(rear_sysml)
Now we’ll create an Engine object and convert it to a SysML v2 part.
# Engine object
class Engine:
def __init__(self, power, mass, cylinders):
self.power = power
self.power_unit = 'hp'
self.mass = mass
self.mass_unit = 'kg'
self.cylinders = cylinders
# Create an object of the Engine class
engine = Engine(power=200, mass=300, cylinders=6)
# Convert the Engine object to a SysML v2 part
engine_sysml = sysml2_part("engine", engine)
print(engine_sysml)
Writing the SysML v2 to a *.sysml file
To export our SysML v2 to a *.sysml file, we need to combine the SysML v2 strings we’ve written. Then we’ll export them to a .sysml file that we enable the user to name.
# Combine SysML v2 parts
full_sysml=f"{front_sysml}\n\n{rear_sysml}\n\n{engine_sysml}"
print(full_sysml)
# Function to write to file
def write_sysml(content: str):
while True:
filename = input("Enter the filename to save the SysML content (e.g., Vehicle.sysml): ")
if not filename.endswith('.sysml'):
filename += '.sysml'
try:
with open(filename, "w") as file:
file.write(content)
print(f"SysML content has been written to {filename}")
break
except IOError as e:
print(f"An error occurred while writing to the file: {e}")
retry = input("Would you like to try again with a different filename? (y/n): ")
if retry.lower() != 'y':
print("SysML content was not saved.")
break
# Write SysML v2 to file
write_sysml(full_sysml)
Generative AI Usage
Model Diagramming
Data Input and Analysis
upload Excel file and process it with Pandas
import pandas as pd
# create dataframe
df = pd.read_excel('A5_Tutorial_Registration_Report_25June2024.xlsx', header=5)
print(df)
dassault_registrants = df[df['Company Name'].str.contains('Dassault', case=False)]
print('Dassault Registrants\n', dassault_registrants)
# show the columns
print(df.columns)
italicized text
.. |image0| image:: https://github.com/se-lib/se-lib/raw/main/docs/figures/use_case_example.png
- width:
600px