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.


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.

|image0|


Use Case Models

use_case_models2


Sequence Models

battle_planner_sequence_model

legislative_sequence_model


Discrete Event Modeling

image4

image5


Continuous Systems Modeling with System Dynamics

image6

image7

image8


Graphical User Interface

image9

image10


Causal Analysis

image11

image12


Requirements Modeling

image13

image14


Project Modeling

image15

image16

image17


Integrated Requirements, Effort and Staffing Models

image18


Quantitative Fault Tree Analysis

image19

image20


Excel Data Import

image21

image22


se-lib Further Information


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

5 + 3 = 8

-

Subtraction

Subtracts the right operand from the left operand

5 - 3 = 2

*

Multiplication

Multiplies two operands

5 * 3 = 15

/

Division

Divides the left operand by the right operand

5 / 3 = 1.6666...

//

Floor Division

Divides and rounds down to the nearest integer

5 // 3 = 1

%

Modulus

Returns the remainder of the division

5 % 3 = 2

**

Exponentiation

Raises the left operand to the power of the right

5 ** 3 = 125

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

  1. The division operator (/) always returns a float in Python 3.x.

  2. Floor division (//) discards the fractional part, effectively rounding down.

  3. The modulus operator (%) is useful for determining if a number is even or odd.

  4. 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:

  1. and: Returns True if both operands are True

  2. or: Returns True if at least one operand is True

  3. not: 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

5 == 5

!=

Not equal to

Returns True if operands are not equal

5 != 3

>

Greater than

Returns True if left operand is greater than the right

5 > 3

<

Less than

Returns True if left operand is less than the right

3 < 5

>=

Greater than or equal to

Returns True if left operand is greater than or equal to the right

5 >= 5

<=

Less than or equal to

Returns True if left operand is less than or equal to the right

3 <= 5

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

  1. The equality operator (==) checks for value equality, not identity. For identity comparison, use the is operator.

  2. Be careful not to confuse the assignment operator (=) with the equality comparison operator (==).

  3. Comparison operators can also be used with strings, where the comparison is based on lexicographical order.

  4. 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

  1. if statement:

  2. This is the starting point of a conditional block.

  3. If the condition is True, the indented code below it executes.

  4. If False, Python moves to the next condition (if any).

  5. elif statement:

  6. Short for “else if”.

  7. Used to check multiple conditions.

  8. You can have multiple elif statements.

  9. Executed only if all previous conditions were False.

  10. else statement:

  11. Optional. Used at the end of a conditional block.

  12. Executes if all previous conditions were False.

  13. 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

--------------

image23


--------------

image24


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

arr = np.array([1, 2, 3])

Set data type

arr = np.array([12, 23, 43], dtype=float)

Access elements with slicing

arr[0], arr[1:3]

Get array shape

arr.shape

Change data type

arr.astype(int)

Operations

Element-wise operations

arr1 + arr2, arr * 2

Mathematical functions

np.sin(arr), np.sqrt(arr), etc.

Linear algebra

np.dot(arr1, arr2), np.linalg.inv(arr)

Array Manipulation

Reshape arrays

arr.reshape(2, 3)

Concatenate arrays

np.concatenate([arr1, arr2])

Split arrays

np.split(arr, 2)

Copy arrays

arr_copy = arr.copy()

Statistical Functions

Mean, Median, etc.

np.mean(arr), np.median(arr)

Minimum, Maximum

np.min(arr), np.max(arr)

Standard deviation

np.std(arr)

Random Numbers

Generate random numbers

np.random.rand(2, 3) (uniform)

Generate random integers

np.random.randint(1, 10, size=5)

File Input and Output

Save array

np.save('data.npy', arr)

Load array

loaded_arr = np.load('data.npy')

Linear Algebra

Matrix multiplication

np.dot(arr1, arr2)

Solving equations

np.linalg.solve(A, b)

Eigenvalues and eigenvectors

np.linalg.eig(arr)

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:

arr = np.array([1, 2, 3])

* With data type:

arr = np.array([1, 2, 3], dtype=float)

Access elements

arr[0], arr[1:3](slicing)

Get array shape

arr.shape

Change data type

arr.astype(int)

Operations

Element-wise operations

arr1 + arr2, arr * 2

Mathematical functions

np.sin(arr), np.sqrt(arr), etc.

Linear algebra

np.dot(arr1, arr2), np.linalg.inv(arr)

Array Manipulation

Reshape arrays

arr.reshape(2, 3)

Concatenate arrays

np.concatenate([arr1, arr2])

Split arrays

np.split(arr, 2)

Copy arrays

arr_copy = arr.copy()(avoid modifying original)

Statistical Functions

Mean, Median, etc.

np.mean(arr), np.median(arr)

Minimum, Maximum

np.min(arr), np.max(arr)

Standard deviation

np.std(arr)

Random Numbers

Generate random numbers

np.random.rand(2, 3)(uniform)

Generate random integers

np.random.randint(1, 10, size=5)

Input & Output

Save array

np.save('data.npy', arr)

Load array

loaded_arr = np.load('data.npy')

Linear Algebra

Matrix multiplication

np.dot(arr1, arr2)

Solving equations

np.linalg.solve(A, b)

Eigenvalues & eigenvectors

np.linalg.eig(arr)


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()

--------------

image25



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.


image.png


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()

--------------

image27


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()

--------------

image28


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)

--------------
--------------

image29


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

df = pd.read_csv('data.csv')

Load Excel (xlsx)

df = pd.read_excel('data.xlsx')

Save CSV

df.to_csv(’data.csv, index=False)`

Save Excel (xlsx)

df.to_excel(’data.xlsx’, index=False)

Selection and Indexing

Select column(s)

df[[’column1’, ’column2’]]

Select rows by label

df.loc[`row_label]`

Select rows by condition

df[df[’column’] > value]

Get column by label

df[row_label]

Manipulation

Add new column

df['new_column'] = ...

Remove column

del df[’column’]

Add/Remove rows (append/drop)

df.append(...)', 'df.drop(index)

Rename columns

df.rename(columns=’old_name’: ’new_name’)

Sort DataFrame

df.sort_values(by=’column’)

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

A school is building a course portal for instructor and student use. For the system context diagram below, modify the context diagram to include additional elements that should be included. If some elements should be deleted, remove them from the 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.

Given the parameters above, how many ships are expected to exceed 10 days of waiting for a dry dock?

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

image.png


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