├── .gitignore
├── LICENSE
├── README.md
├── __init__.py
├── config.example.json
├── config.py
├── log.py
├── main.py
├── requirements.txt
├── server
├── __init__.py
├── client_events
│ ├── __init__.py
│ ├── client_button_parameters_update_event.py
│ ├── client_chat_event.py
│ ├── client_player_position_parameters_update_event.py
│ ├── client_recorder_parameters_update_event.py
│ ├── client_rod_select_update_event.py
│ ├── client_switch_parameters_update_event.py
│ └── client_voip_event.py
├── connection_manager.py
├── constants
│ ├── __init__.py
│ └── packets.py
├── helpers
│ ├── __init__.py
│ └── packet_helper.py
├── init_server.py
├── rcon.py
└── server_events
│ ├── __init__.py
│ ├── server_alarm_parameters_update_event.py
│ ├── server_button_parameters_update_event.py
│ ├── server_chat_event.py
│ ├── server_indicator_parameters_update_event.py
│ ├── server_meter_parameters_update_event.py
│ ├── server_player_position_parameters_update_event.py
│ ├── server_recorder_parameters_update_event.py
│ ├── server_rod_position_parameters_update_event.py
│ ├── server_switch_parameters_update_event.py
│ ├── server_user_logout_event.py
│ └── server_voip_event.py
└── simulation
├── __init__.py
├── constants
├── annunciator_states.py
├── electrical_types.py
├── equipment_states.py
├── fluid.py
└── pump.py
├── init_simulation.py
└── models
├── control_room_columbia
├── annunciators.py
├── general_physics
│ ├── ac_power.py
│ ├── accidents.py
│ ├── air_system.py
│ ├── dc_power.py
│ ├── diesel_generator.py
│ ├── fluid.py
│ ├── gas.py
│ ├── main_condenser.py
│ ├── main_generator.py
│ ├── main_turbine.py
│ ├── pump.py
│ ├── tank.py
│ ├── turbine.py
│ └── turbine_new.py
├── libraries
│ ├── pid.py
│ └── transient.py
├── model.py
├── neutron_monitoring.py
├── reactor_physics
│ ├── cross_sections.py
│ ├── fuel.py
│ ├── neutrons.py
│ ├── pressure.py
│ ├── reactor_inventory.py
│ ├── reactor_physics.py
│ └── steam_functions.py
├── reactor_protection_system.py
├── rod_drive_control_system.py
├── rod_generation.py
├── rod_position_information_system.py
└── systems
│ ├── ESFAS.py
│ ├── cas.py
│ ├── chart.py
│ ├── cia.py
│ ├── circ_water.py
│ ├── condensate.py
│ ├── control_rod_drive.py
│ ├── deh.py
│ ├── diesels.py
│ ├── ehc.py
│ ├── feedwater.py
│ ├── fire.py
│ ├── fire_control_panel.py
│ ├── fukushima.py
│ ├── hpcs.py
│ ├── irm_srm_positioner.py
│ ├── loop_sequence.py
│ ├── lpcs.py
│ ├── pcis.py
│ ├── rcic.py
│ ├── residual_heat_removal.py
│ ├── rod_worth_minimizer.py
│ ├── rrc.py
│ ├── safety_relief.py
│ ├── service_water.py
│ └── sync.py
└── dev_test
└── model.py
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | share/python-wheels/
24 | *.egg-info/
25 | .installed.cfg
26 | *.egg
27 | MANIFEST
28 |
29 | # PyInstaller
30 | # Usually these files are written by a python script from a template
31 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
32 | *.manifest
33 | *.spec
34 |
35 | # Installer logs
36 | pip-log.txt
37 | pip-delete-this-directory.txt
38 |
39 | # Unit test / coverage reports
40 | htmlcov/
41 | .tox/
42 | .nox/
43 | .coverage
44 | .coverage.*
45 | .cache
46 | nosetests.xml
47 | coverage.xml
48 | *.cover
49 | *.py,cover
50 | .hypothesis/
51 | .pytest_cache/
52 | cover/
53 |
54 | # Translations
55 | *.mo
56 | *.pot
57 |
58 | # Django stuff:
59 | *.log
60 | local_settings.py
61 | db.sqlite3
62 | db.sqlite3-journal
63 |
64 | # Flask stuff:
65 | instance/
66 | .webassets-cache
67 |
68 | # Scrapy stuff:
69 | .scrapy
70 |
71 | # Sphinx documentation
72 | docs/_build/
73 |
74 | # PyBuilder
75 | .pybuilder/
76 | target/
77 |
78 | # Jupyter Notebook
79 | .ipynb_checkpoints
80 |
81 | # IPython
82 | profile_default/
83 | ipython_config.py
84 |
85 | # pyenv
86 | # For a library or package, you might want to ignore these files since the code is
87 | # intended to run in multiple environments; otherwise, check them in:
88 | # .python-version
89 |
90 | # pipenv
91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
94 | # install all needed dependencies.
95 | #Pipfile.lock
96 |
97 | # poetry
98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
99 | # This is especially recommended for binary packages to ensure reproducibility, and is more
100 | # commonly ignored for libraries.
101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
102 | #poetry.lock
103 |
104 | # pdm
105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
106 | #pdm.lock
107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
108 | # in version control.
109 | # https://pdm.fming.dev/#use-with-ide
110 | .pdm.toml
111 |
112 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
113 | __pypackages__/
114 |
115 | # Celery stuff
116 | celerybeat-schedule
117 | celerybeat.pid
118 |
119 | # SageMath parsed files
120 | *.sage.py
121 |
122 | # Environments
123 | .env
124 | .venv
125 | env/
126 | venv/
127 | ENV/
128 | env.bak/
129 | venv.bak/
130 |
131 | # Spyder project settings
132 | .spyderproject
133 | .spyproject
134 |
135 | # Rope project settings
136 | .ropeproject
137 |
138 | # mkdocs documentation
139 | /site
140 |
141 | # mypy
142 | .mypy_cache/
143 | .dmypy.json
144 | dmypy.json
145 |
146 | # Pyre type checker
147 | .pyre/
148 |
149 | # pytype static type analyzer
150 | .pytype/
151 |
152 | # Cython debug symbols
153 | cython_debug/
154 |
155 | # PyCharm
156 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
157 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
158 | # and can be added to the global gitignore or merged into this file. For a more nuclear
159 | # option (not recommended) you can uncomment the following to ignore the entire idea folder.
160 | #.idea/
161 |
162 | config.json
163 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # OpenLWR-Server
2 |
3 | Serverside component of OpenLWR, handles communication with clients and runs the simulation calculations.\
4 | very big thanks to Goosey (fluff.goose on discord) for writing the original reactor physics in lua
5 |
6 | # How to run
7 | NOTE: This guide is primarily intended for programmers, and thus assumes you already have knowledge of what is a terminal/command prompt and how to use it. If you do not possess such knowledge, please search the internet for "how to use CMD" or something similar before attempting to follow this guide
8 | ## Step 1: Installing python3.12
9 |
10 |
11 | Windows
12 |
13 | Install python3.12 via the microsoft store\
14 | Can also be installed from the python website, however please make sure you check the option to set the PATH variable in the installer
15 |
16 |
17 |
18 | macOS
19 |
20 | NOTE: macOS 13 "Ventura" or later is required to install python\
21 | Install [brew.sh](https://brew.sh/) via the following command:
22 | ```
23 | /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
24 | ```
25 | Then, install python via homebrew:
26 | ```
27 | brew install python@3.12
28 | ```
29 |
30 |
31 |
32 | Linux
33 |
34 |
35 |
36 |
37 | Ubuntu/Debian
38 |
39 | NOTE: python3.12 is not available on debian stable, in this case, you must build python3.12 yourself in order to run the server. This process will not be covered by this guide.
40 | ```
41 | sudo apt install python3.12 python3-pip
42 | ```
43 |
44 |
45 | Fedora/RHEL
46 |
47 | ```
48 | sudo dnf install python3.12 python3.12-pip
49 | ```
50 |
51 |
52 | Arch
53 |
54 | ```
55 | sudo pacman -S python python-pip
56 | ```
57 |
58 |
59 | Gentoo
60 |
61 | ```
62 | sudo emerge --ask dev-lang/python:3.12 dev-python/pip
63 | ```
64 |
65 |
66 |
67 |
68 | FreeBSD
69 |
70 | NOTE: python3.12 is not available, you must build python3.12 yourself in order to run the server. This process will not be covered by this guide.\
71 | Once python3.12 is installed, you may install pip using the following command.
72 | ```
73 | python3.12 -m ensurepip --upgrade
74 | ```
75 |
76 |
77 | ## Step 2: Installing python dependencies
78 | cd into the directory where the server files are, then run the following command:
79 | ```
80 | python3.12 -m pip install -r requirements.txt
81 | ```
82 |
83 | In some cases, pip may throw an error telling you to create a venv, if pip runs successfully, you may skip this step, however, if this occurrs, you can resolve this with the following:
84 | ```
85 | python3.12 -m venv .
86 |
87 | # Please note that you will need to run this to activate the venv every time you want to start the server
88 | source bin/activate
89 |
90 | # Now run pip again
91 | python3.12 -m pip install -r requirements.txt
92 | ```
93 |
94 |
95 | ## Step 3: Running the server
96 | In the server directory, run the following command:
97 | ```
98 | python3.12 main.py
99 | ```
100 |
--------------------------------------------------------------------------------
/__init__.py:
--------------------------------------------------------------------------------
1 | from . import simulation
2 | from . import server
3 |
--------------------------------------------------------------------------------
/config.example.json:
--------------------------------------------------------------------------------
1 | {
2 | "server_ip": "0.0.0.0",
3 | "server_port": 7001,
4 | "model": "control_room_columbia",
5 | "debug": false,
6 | "discord_webhook_logging" : "",
7 | "blame_logging": false,
8 | "server_motd" : "MOTD",
9 | "version" : "alpha2025223"
10 | }
11 |
--------------------------------------------------------------------------------
/config.py:
--------------------------------------------------------------------------------
1 | import os
2 | import config
3 | import shutil
4 | import json
5 |
6 | config = None
7 |
8 | if config == None:
9 | try:
10 | if not os.path.isfile("config.json"):
11 | print("Config file not found, creating")
12 | shutil.copy("config.example.json", "config.json")
13 |
14 | with open("config.json") as config_file:
15 | config = json.load(config_file)
16 |
17 | if not os.path.isdir("simulation/models/%s" % config["model"]):
18 | print("The configured model directory does not exist.")
19 | exit()
20 |
21 | if not os.path.isfile("simulation/models/%s/model.py" % config["model"]):
22 | print("The configured model has a directory but no model.py. Did you misspell model.py?")
23 | exit()
24 |
25 | print("The config initialized sucessfully.")
26 |
27 |
28 | except Exception as e:
29 | print(f"Failed to load config file: {e}")
30 |
--------------------------------------------------------------------------------
/log.py:
--------------------------------------------------------------------------------
1 |
2 | PINK = '\033[95m'
3 | BLUE = '\033[94m'
4 | GREEN = '\033[92m'
5 | YELLOW = '\033[93m'
6 | RED = '\033[91m'
7 | ENDC = '\033[0m'
8 | BOLD = '\033[1m'
9 | UNDERLINE = '\033[4m'
10 |
11 | import logging
12 | from dhooks import Webhook, File, Embed #this util allows us to log to discord easily
13 |
14 | import json
15 |
16 | import config
17 |
18 | hook = None
19 |
20 | if config.config["discord_webhook_logging"] != "":
21 | hook = Webhook(config.config["discord_webhook_logging"])
22 |
23 |
24 |
25 | logging.basicConfig( format='[%(asctime)s] %(levelname)s - %(message)s',
26 | level=logging.DEBUG if config.config["debug"] else logging.INFO,
27 | datefmt='%Y-%m-%d %H:%M:%S')
28 |
29 | logger = logging.getLogger()
30 |
31 | def debug(message:str,color:str = None):
32 | """message, *color"""
33 | if color:
34 | message = color + message + ENDC
35 | logger.debug(message)
36 |
37 | def info(message:str,color:str = None):
38 | """message, *color"""
39 | if color:
40 | message = color + message + ENDC
41 | logger.info(message)
42 |
43 | def warning(message:str,color:str = None):
44 | """message, *color"""
45 |
46 | if hook != None:
47 | embed = Embed(title = "Warning",description = message,timestamp = "now",color=0xFFA500)
48 | hook.send(embed=embed)
49 |
50 | if color:
51 | message = color + message + ENDC
52 |
53 | # TODO: add option for logging to other external services(datadog, etc)
54 | logger.warning(message)
55 |
56 | def error(message:str,color:str = None):
57 | """message, *color"""
58 |
59 | #TODO: not all errors are sent because they arent sent through log.py. Is there a way to include them?
60 | if hook != None:
61 | embed = Embed(title = "Error",description = message,timestamp = "now",color=0xFF0000)
62 | hook.send(embed=embed)
63 |
64 | if color:
65 | message = color + message + ENDC
66 |
67 |
68 | # TODO: add option for logging to other external services(datadog, etc)
69 | logger.error(message)
70 |
71 | def blame(user,message): #TODO: More elegant way of doing this?
72 | if config.config["blame_logging"]:
73 | print("Server blames %s for %s" % (user,message))
74 |
--------------------------------------------------------------------------------
/main.py:
--------------------------------------------------------------------------------
1 | from websockets.sync.server import serve
2 | from websockets.http11 import Response
3 | from websockets.datastructures import Headers
4 | import config
5 | from server import init_server
6 | import threading
7 | import json
8 |
9 |
10 |
11 | def main():
12 | with serve(init_server.init_server, config.config["server_ip"], config.config["server_port"], process_request=process_request) as server:
13 | print(f"> Listening for clients on {config.config["server_ip"]}:{config.config["server_port"]}")
14 | server.serve_forever()
15 |
16 | def process_request(connection, headers):
17 | path = connection.request.path
18 | if path == "/status":
19 | response = bytes(json.dumps({'status': '{}'.format(config.config["model"]), 'model': config.config["model"], 'motd': motd()}),'UTF-8')
20 | return Response(200, "OK", Headers({'Content-Type': 'application/json', 'Content-Length': len(response)}),
21 | response)
22 | return None
23 |
24 | def motd():
25 | return config.config["server_motd"]
26 |
27 | def import_simulation():
28 | import simulation
29 |
30 | if __name__ == '__main__':
31 | print(""" ____ __ _ ______
32 | / __ \\____ ___ ____ / /| | / / __ \\
33 | / / / / __ \\/ _ \\/ __ \\/ / | | /| / / /_/ /
34 | / /_/ / /_/ / __/ / / / /__| |/ |/ / _, _/
35 | \\____/ .___/\\___/_/ /_/_____/__/|__/_/ |_|
36 | /_/ \n""")
37 | print("> Welcome to OpenLWR-Server")
38 | import config
39 | if config.config != None:
40 | import log
41 | threading.Thread(target=import_simulation).start()
42 | main()
43 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | websockets==12.0
2 | iapws==1.5.4
3 | dhooks==1.1.4
--------------------------------------------------------------------------------
/server/__init__.py:
--------------------------------------------------------------------------------
1 | from . import connection_manager
2 | from . import helpers
3 | from . import server_events
4 | from . import client_events
5 | from . import constants
6 | from . import init_server
--------------------------------------------------------------------------------
/server/client_events/__init__.py:
--------------------------------------------------------------------------------
1 | from . import client_switch_parameters_update_event
2 | from . import client_button_parameters_update_event
--------------------------------------------------------------------------------
/server/client_events/client_button_parameters_update_event.py:
--------------------------------------------------------------------------------
1 | import json
2 | from server.server_events import server_button_parameters_update_event
3 | import config
4 | import importlib
5 | import log
6 |
7 | def handle(data):
8 | model = importlib.import_module(f"simulation.models.{config.config["model"]}.model")
9 | try:
10 | buttons_updated = json.loads(data)
11 | except json.decoder.JSONDecodeError:
12 | log.error(f"Failed to decode json: {data}")
13 |
14 | log.debug(buttons_updated)
15 | if type(buttons_updated) == dict and len(buttons_updated) >= 1:
16 | for button in buttons_updated:
17 | if button in model.buttons:
18 | model.buttons[button] = buttons_updated[button]
19 | else:
20 | log.warning(f"Got a button that is invalid from client: {button}")
21 |
22 | server_button_parameters_update_event.fire(data)
23 |
24 | else:
25 | log.warning(f"Got a button state packet that is invalid: {buttons_updated}")
26 |
--------------------------------------------------------------------------------
/server/client_events/client_chat_event.py:
--------------------------------------------------------------------------------
1 | import json
2 | from server.server_events import server_chat_event
3 |
4 | def handle(data, source_user):
5 | server_chat_event.fire("{1}: {0}".format(data, source_user))
6 |
--------------------------------------------------------------------------------
/server/client_events/client_player_position_parameters_update_event.py:
--------------------------------------------------------------------------------
1 | import json
2 | from server.server_events import server_player_position_parameters_update_event
3 | import log
4 |
5 | def handle(data, token):
6 | try:
7 | players_updated = json.loads(data)
8 | except json.decoder.JSONDecodeError:
9 | log.error(f"Failed to decode json: {data}")
10 | #get the players whos position updated
11 |
12 | if type(players_updated) == dict and len(players_updated) == 1:
13 | token.position = players_updated[token.username]["position"]
14 | token.rotation = players_updated[token.username]["rotation"]
15 | server_player_position_parameters_update_event.fire()
16 | else:
17 | log.warning(f"Got a player position packet that is invalid: {players_updated}")
--------------------------------------------------------------------------------
/server/client_events/client_recorder_parameters_update_event.py:
--------------------------------------------------------------------------------
1 | import json
2 | import config
3 | import importlib
4 | import log
5 |
6 | def handle(data):
7 | model = importlib.import_module(f"simulation.models.{config.config["model"]}.model")
8 | try:
9 | data = data.split("|")
10 | chart_name = data[0]
11 | buttons_updated = json.loads(data[1])
12 | except json.decoder.JSONDecodeError:
13 | log.error(f"Failed to decode json: {data}")
14 |
15 | if type(buttons_updated) == dict and len(buttons_updated) >= 1:
16 | for button in buttons_updated:
17 | model.recorders[chart_name].button_updated(button,buttons_updated[button])
18 | else:
19 | log.warning(f"Got a recorder state packet that is invalid.")
20 |
--------------------------------------------------------------------------------
/server/client_events/client_rod_select_update_event.py:
--------------------------------------------------------------------------------
1 | import json
2 | import config
3 | import importlib
4 | import log
5 |
6 | def handle(data):
7 | model = importlib.import_module(f"simulation.models.{config.config["model"]}.model")
8 | try:
9 | rod_selected = json.loads(data)
10 | except json.decoder.JSONDecodeError:
11 | log.error(f"Failed to decode json: {data}")
12 |
13 | if type(rod_selected) == str and len(rod_selected) >= 1:
14 | if rod_selected in model.rods:
15 | from simulation.models.control_room_columbia import rod_drive_control_system
16 | if not rod_drive_control_system.select_block:
17 | for rod in model.rods:
18 | model.rods[rod]["select"] = False
19 |
20 | model.rods[rod_selected]["select"] = True
21 |
22 |
23 |
24 | else:
25 | log.warning(f"Got a rod select that is invalid from client: {rod_selected}")
26 |
27 | else:
28 | log.warning(f"Got a rod select packet that is invalid: {rod_selected}")
29 |
--------------------------------------------------------------------------------
/server/client_events/client_switch_parameters_update_event.py:
--------------------------------------------------------------------------------
1 | import json
2 | from server.server_events import server_switch_parameters_update_event
3 | import config
4 | import importlib
5 | import log
6 |
7 | def handle(data):
8 | model = importlib.import_module(f"simulation.models.{config.config["model"]}.model")
9 |
10 | try:
11 | switches_updated = json.loads(data)
12 | except json.decoder.JSONDecodeError:
13 | log.error(f"Failed to decode json: {data}")
14 |
15 | if type(switches_updated) == dict and len(switches_updated) >= 1:
16 | for switch in switches_updated:
17 | if switch in model.switches and "position" in model.switches[switch]:
18 | model.switches[switch]["position"] = switches_updated[switch]["position"]
19 | #should we check if this is correct also?
20 | model.switches[switch]["flag"] = switches_updated[switch]["flag"]
21 | else:
22 | log.warning(f"Got a switch that is invalid from client: {switch}")
23 |
24 | server_switch_parameters_update_event.fire(switches_updated)
25 | else:
26 | log.warning(f"Got a switch position packet that is invalid: {switches_updated}")
27 |
--------------------------------------------------------------------------------
/server/client_events/client_voip_event.py:
--------------------------------------------------------------------------------
1 | from server.server_events import server_voip_event
2 |
3 |
4 | def handle(data, source_user):
5 | server_voip_event.fire(data,source_user)
6 |
--------------------------------------------------------------------------------
/server/connection_manager.py:
--------------------------------------------------------------------------------
1 | import traceback
2 | import websockets
3 | import log
4 |
5 | class Token:
6 | def __init__(self, websocket, username, token):
7 | self.websocket = websocket
8 | self.username = username
9 | self.position = {"x":0,"y":0,"z":0}
10 | self.rotation = {"x":0,"y":0,"z":0} #TODO: Move somewhere else?
11 | self.token = token
12 |
13 | class ConnectionManager:
14 | def __init__(self):
15 | self.tokens = {}
16 |
17 | def connect(self, websocket, token: str):
18 | self.tokens[token] = Token(websocket, "No Username", token)
19 | return self.tokens[token]
20 |
21 | def disconnect(self, token: str):
22 | if token in self.tokens:
23 | self.tokens.pop(token)
24 | else:
25 | log.warning(f"attempted to disconnect nonexistent client: {token}")
26 |
27 | def send_user_packet(self, message: str, token: str):
28 | try:
29 | self.tokens[token].websocket.send(message)
30 | except Exception:
31 | log.error(traceback.format_exc())
32 |
33 | def broadcast_packet(self, message: str):
34 | try:
35 | token_list = self.tokens.copy() #prevents "dictionary changed size during iteration" error
36 | for token in token_list:
37 | try:
38 | self.tokens[token].websocket.send(message)
39 | except websockets.exceptions.ConnectionClosedOK:
40 | self.disconnect(token)
41 | except Exception:
42 | log.error(traceback.format_exc())
43 | except RuntimeError:
44 | log.error(traceback.format_exc())
45 |
46 |
47 |
48 | def set_username(self, token: str, username: str):
49 | self.tokens[token].username = username
50 |
51 | manager = ConnectionManager()
52 |
--------------------------------------------------------------------------------
/server/constants/__init__.py:
--------------------------------------------------------------------------------
1 | from . import packets
--------------------------------------------------------------------------------
/server/constants/packets.py:
--------------------------------------------------------------------------------
1 | from enum import IntEnum
2 |
3 | class ClientPackets(IntEnum):
4 | SWITCH_PARAMETERS_UPDATE = 2
5 | BUTTON_PARAMETERS_UPDATE = 6
6 | PLAYER_POSITION_PARAMETERS_UPDATE = 9
7 | ROD_SELECT_UPDATE = 11,
8 | USER_LOGIN = 12,
9 | SYNCHRONIZE = 14,
10 | CHAT = 15,
11 | RCON = 19,
12 | RECORDER = 21,
13 | VOIP = 22,
14 |
15 | class ServerPackets(IntEnum):
16 | METER_PARAMETERS_UPDATE = 0
17 | USER_LOGOUT = 1
18 | SWITCH_PARAMETERS_UPDATE = 3
19 | INDICATOR_PARAMETERS_UPDATE = 4
20 | ALARM_PARAMETERS_UPDATE = 5
21 | BUTTON_PARAMETERS_UPDATE = 7
22 | PLAYER_POSITION_PARAMETERS_UPDATE = 8
23 | ROD_POSITION_PARAMETERS_UPDATE = 10
24 | USER_LOGIN_ACK = 13,
25 | CHAT = 16,
26 | DOWNLOAD_DATA = 17,
27 | KICK = 18,
28 | RECORDER = 20,
29 | VOIP = 23,
30 |
--------------------------------------------------------------------------------
/server/helpers/__init__.py:
--------------------------------------------------------------------------------
1 | from . import packet_helper
--------------------------------------------------------------------------------
/server/helpers/packet_helper.py:
--------------------------------------------------------------------------------
1 | import base64
2 |
3 | def build(packet_id, data = ""):
4 | return f"{packet_id}|{base64.b64encode(data.encode('utf-8')).decode('utf-8')}"
5 |
6 | def parse(packet):
7 | packet = packet.split("|")
8 | return int(packet[0]), base64.b64decode(packet[1]).decode('utf-8')
--------------------------------------------------------------------------------
/server/init_server.py:
--------------------------------------------------------------------------------
1 | import uuid
2 | import traceback
3 | import websockets
4 | from server.connection_manager import manager
5 | from server.server_events import server_user_logout_event
6 | from server.constants import packets
7 | from server.helpers import packet_helper
8 | from server.client_events import client_switch_parameters_update_event
9 | from server.server_events import server_switch_parameters_update_event
10 | from server.client_events import client_button_parameters_update_event
11 | from server.server_events import server_button_parameters_update_event
12 | from server.server_events import server_player_position_parameters_update_event
13 | from server.client_events import client_player_position_parameters_update_event
14 | from server.server_events import server_rod_position_parameters_update_event
15 | from server.client_events import client_rod_select_update_event
16 | from server.server_events import server_chat_event
17 | from server.client_events import client_chat_event
18 | from server.client_events import client_voip_event
19 | from server.client_events import client_recorder_parameters_update_event
20 | from server.server_events import server_recorder_parameters_update_event
21 | from server.server_events import server_meter_parameters_update_event
22 | import json
23 | import importlib
24 | import copy
25 | import time
26 | from server import rcon
27 | rcon_init = False
28 |
29 | def init_server(websocket):
30 | import log
31 | import config
32 | if not rcon_init:
33 | rcon.init()
34 |
35 | login_step = 0
36 | model_copy = None
37 |
38 | try:
39 | for packet in websocket:
40 | try:
41 | if login_step == 0:
42 | token_str = str(uuid.uuid4())
43 | packet_id, packet_data = packet_helper.parse(packet)
44 |
45 | login_parameters = json.loads(packet_data)
46 |
47 | username = login_parameters["username"]
48 | version = login_parameters["version"]
49 |
50 | # check the packet is the correct type and the username is valid
51 | assert packet_id == packets.ClientPackets.USER_LOGIN, "Client sent an invalid packet while logging in."
52 | assert (len(username) <= 20 and len(username) >= 2), "Client has an invalid username."
53 |
54 | # check to make sure the client has the same version as the server
55 | assert version == config.config["version"], f"Version mismatch. Client has {version} - Server has {config.config["version"]}"
56 |
57 | login_step += 1
58 |
59 | #before logging in the client, we need to actually give them all the data
60 | #make a copy of the current state of model.py
61 |
62 | model = importlib.import_module(f"simulation.models.{config.config["model"]}.model")
63 |
64 | recorders = {}
65 | model_recorders = model.recorders.copy()
66 | for recorder in model_recorders:
67 | recorders[recorder] = {
68 | "channels":model_recorders[recorder].channels,
69 | "buttons":model_recorders[recorder].buttons,
70 | "page":model_recorders[recorder].page,
71 | "elements":model_recorders[recorder].elements,
72 | "display_on":model_recorders[recorder].display_on,
73 | }
74 |
75 | model_rods = copy.deepcopy(model.rods) #sanitize rods sent to the client
76 | for rod in model_rods:
77 | model_rods[rod].pop("neutrons")
78 | model_rods[rod].pop("measured_neutrons_last")
79 | model_rods[rod].pop("measured_neutrons")
80 | model_rods[rod].pop("driftto")
81 | model_rods[rod].pop("driving")
82 | model_rods[rod].pop("reed_switch_fail")
83 | model_rods[rod].pop("accum_pressure")
84 |
85 | model_copy = {
86 | "switches" : model.switches.copy(),
87 | #"values" : model.values.copy(), #This will stay on the client
88 | "buttons" : model.buttons.copy(),
89 | #"indicators" : model.indicators.copy(), #This will stay on the client
90 | "alarms" : model.alarms.copy(),
91 | "rods" : model_rods,
92 | "recorders" : recorders,
93 | }
94 |
95 | if login_step > 0:
96 | if login_step <= 5:
97 | i = 1
98 | for table_name in model_copy:
99 | if i == login_step:
100 | info = model_copy[table_name]
101 |
102 | data_to_send = f"{json.dumps(info)}|{json.dumps(table_name)}" #Do we actually need to json.dumps the table name?
103 |
104 | websocket.send(packet_helper.build(packets.ServerPackets.DOWNLOAD_DATA,data_to_send))
105 | break
106 | i+=1
107 | else:
108 |
109 | #TODO: Checksum the client against the server
110 |
111 | #last, we finally broadcast the login packet
112 | websocket.send(packet_helper.build(packets.ServerPackets.USER_LOGIN_ACK,"ok"))
113 |
114 | token_object = manager.connect(websocket, token_str)
115 | manager.set_username(token_str, username)
116 | break
117 | login_step+=1
118 |
119 |
120 | except AssertionError as msg:
121 | # login invalid
122 | log.warning(f"User login failed for {msg}")
123 | manager.send_user_packet(packet_helper.build(packets.ServerPackets.USER_LOGIN_ACK,str(msg)),token_object.token)
124 | manager.disconnect(token_str)
125 |
126 | except Exception:
127 | # exception in login
128 | log.error(traceback.format_exc())
129 |
130 | except websockets.exceptions.ConnectionClosedError:
131 | log.info("Connection closed.")
132 | return #this works but should we do this?
133 |
134 |
135 | try:
136 | for packet in websocket:
137 | packet_id, packet_data = packet_helper.parse(packet)
138 | match packet_id:
139 | case packets.ClientPackets.SWITCH_PARAMETERS_UPDATE:
140 | client_switch_parameters_update_event.handle(packet_data)
141 | log.blame(token_object.username,packet_data)
142 |
143 | case packets.ClientPackets.BUTTON_PARAMETERS_UPDATE:
144 | client_button_parameters_update_event.handle(packet_data)
145 | log.blame(token_object.username,packet_data)
146 |
147 | case packets.ClientPackets.PLAYER_POSITION_PARAMETERS_UPDATE:
148 | client_player_position_parameters_update_event.handle(packet_data,token_object)
149 |
150 | case packets.ClientPackets.ROD_SELECT_UPDATE:
151 | client_rod_select_update_event.handle(packet_data)
152 | log.blame(token_object.username,packet_data)
153 |
154 | case packets.ClientPackets.SYNCHRONIZE: #allows the client to request some extra stuff
155 | server_player_position_parameters_update_event.fire_initial(token_object.token)
156 | server_meter_parameters_update_event.fire_initial(token_object.token)
157 |
158 | case packets.ClientPackets.CHAT:
159 | client_chat_event.handle(packet_data, token_object.username)
160 |
161 | case packets.ClientPackets.VOIP:
162 | client_voip_event.handle(packet_data, token_object.username)
163 |
164 | case packets.ClientPackets.RCON:
165 | rcon.process_rcon(packet_data)
166 |
167 | case packets.ClientPackets.RECORDER:
168 | client_recorder_parameters_update_event.handle(packet_data)
169 |
170 | except websockets.exceptions.ConnectionClosedOK:
171 | manager.disconnect(token_str)
172 | server_user_logout_event.fire(token_object.username)
173 | except Exception:
174 | log.error(traceback.format_exc())
175 | manager.disconnect(token_str)
176 | server_user_logout_event.fire(token_object.username)
177 |
--------------------------------------------------------------------------------
/server/rcon.py:
--------------------------------------------------------------------------------
1 | import config
2 | import importlib
3 |
4 | def getVal(cmd:str):
5 |
6 | #Goes : Action Area Value
7 |
8 | assert cmd[1] in areas, "Specified area does not exist."
9 | assert cmd[2] in areas[cmd[1]], "Specified value in area does not exist."
10 |
11 | return areas[cmd[1]][cmd[2]]
12 |
13 | areas = {}
14 | actions = {}
15 |
16 | def init():
17 | model = importlib.import_module(f"simulation.models.{config.config["model"]}.model")
18 |
19 | areas = {
20 | "switches" : model.switches,
21 | }
22 |
23 | actions = {
24 | "getVal" : getVal
25 | }
26 |
27 |
28 | def process_rcon(data):
29 | #I havent got a chance to test this or implement it clientside
30 | try:
31 |
32 | assert config.config["debug"], "The server is not in debug mode."
33 |
34 | cmd = data
35 |
36 | cmd = cmd.lower() #non case-sensitive
37 | cmd = cmd.split(" ") #split at the spaces
38 | if cmd[0] in actions:
39 | #return actions[cmd[0]](cmd)
40 | val = actions[cmd[0]](cmd)
41 |
42 | except AssertionError as msg:
43 | #invalid
44 | print(msg)
45 |
46 | except Exception as e:
47 | print(e)
--------------------------------------------------------------------------------
/server/server_events/__init__.py:
--------------------------------------------------------------------------------
1 | from . import server_meter_parameters_update_event
2 | from . import server_user_logout_event
3 | from . import server_switch_parameters_update_event
4 | from . import server_indicator_parameters_update_event
5 | from . import server_alarm_parameters_update_event
6 | from . import server_button_parameters_update_event
7 | from . import server_player_position_parameters_update_event
--------------------------------------------------------------------------------
/server/server_events/server_alarm_parameters_update_event.py:
--------------------------------------------------------------------------------
1 | from server.helpers import packet_helper
2 | from server.connection_manager import manager
3 | from server.constants import packets
4 | import json
5 |
6 | from enum import IntEnum
7 |
8 | class AnnunciatorStates(IntEnum):
9 | CLEAR = 0
10 | ACTIVE = 1
11 | ACKNOWLEDGED = 2
12 | ACTIVE_CLEAR = 3
13 |
14 | def fire(alarms): #TODO only send ones that need to be updated (were changed)
15 | data = {}
16 | silent = {
17 | "1" : {"F":True,"S":True}, # F - Fast S - Slow
18 | "2" : {"F":True,"S":True},
19 | "3" : {"F":True,"S":True},
20 | "4" : {"F":True,"S":True},
21 | "5" : {"F":True,"S":True},
22 | "6" : {"F":True,"S":True},
23 | "7" : {"F":True,"S":True}, #Div 1
24 | "8" : {"F":True,"S":True}, #Nondiv
25 | "9" : {"F":True,"S":True}, #Div 2
26 | }
27 |
28 | for alarm in alarms:
29 | data[alarm] = {"state" : alarms[alarm]["state"],"silenced" : alarms[alarm]["silenced"]}
30 | alarm = alarms[alarm]
31 | if alarm["group"] == "-1":
32 | continue
33 | if not alarm["silenced"] and (alarm["state"] == AnnunciatorStates.ACTIVE):
34 | silent[alarm["group"]]["F"] = False
35 |
36 | if not alarm["silenced"] and (alarm["state"] == AnnunciatorStates.ACTIVE_CLEAR):
37 | silent[alarm["group"]]["S"] = False
38 |
39 | manager.broadcast_packet(packet_helper.build(packets.ServerPackets.ALARM_PARAMETERS_UPDATE, f"{json.dumps(data)}|{json.dumps(silent)}"))
40 |
41 | # send initial state of indicators
42 | def fire_initial(token):
43 | silent = {
44 | "1" : {"F":True,"S":True}, # F - Fast S - Slow
45 | "2" : {"F":True,"S":True},
46 | }
47 | #manager.send_user_packet(packet_helper.build(packets.ServerPackets.ALARM_PARAMETERS_UPDATE, f"{json.dumps(alarms)}|{json.dumps(silent)}"),token)
--------------------------------------------------------------------------------
/server/server_events/server_button_parameters_update_event.py:
--------------------------------------------------------------------------------
1 | from server.helpers import packet_helper
2 | from server.connection_manager import manager
3 | from server.constants import packets
4 | import json
5 | import config
6 | import importlib
7 |
8 | def fire(buttons): #TODO only send ones that need to be updated (were changed)
9 | manager.broadcast_packet(packet_helper.build(packets.ServerPackets.BUTTON_PARAMETERS_UPDATE, buttons))
10 |
11 | # send initial state of indicators
12 | def fire_initial(token):
13 | model = importlib.import_module(f"simulation.models.{config.config["model"]}.model")
14 | manager.send_user_packet(packet_helper.build(packets.ServerPackets.BUTTON_PARAMETERS_UPDATE, json.dumps(model.buttons)), token)
--------------------------------------------------------------------------------
/server/server_events/server_chat_event.py:
--------------------------------------------------------------------------------
1 | from server.helpers import packet_helper
2 | from server.connection_manager import manager
3 | from server.constants import packets
4 | import json
5 |
6 | def fire(message):
7 | manager.broadcast_packet(packet_helper.build(packets.ServerPackets.CHAT, message))
8 |
9 | def fire_initial(token_object):
10 | manager.send_user_packet(packet_helper.build(packets.ServerPackets.CHAT, "Connected"), token_object.token)
11 | pass
12 |
--------------------------------------------------------------------------------
/server/server_events/server_indicator_parameters_update_event.py:
--------------------------------------------------------------------------------
1 | from server.helpers import packet_helper
2 | from server.connection_manager import manager
3 | from server.constants import packets
4 | import json
5 | import config
6 | import importlib
7 |
8 | def fire(indicators): #TODO only send ones that need to be updated (were changed)
9 | manager.broadcast_packet(packet_helper.build(packets.ServerPackets.INDICATOR_PARAMETERS_UPDATE, json.dumps(indicators)))
10 |
11 | # send initial state of indicators
12 | def fire_initial(token):
13 | model = importlib.import_module(f"simulation.models.{config.config["model"]}.model")
14 | manager.send_user_packet(packet_helper.build(packets.ServerPackets.INDICATOR_PARAMETERS_UPDATE, json.dumps(model.indicators)), token)
--------------------------------------------------------------------------------
/server/server_events/server_meter_parameters_update_event.py:
--------------------------------------------------------------------------------
1 | from server.helpers import packet_helper
2 | from server.connection_manager import manager
3 | from server.constants import packets
4 | import json
5 | import config
6 | import importlib
7 | import copy
8 | old_values = {}
9 | def fire(values):
10 | to_send = {}
11 | global old_values
12 | for value in values:
13 | if value in old_values:
14 | if old_values[value] != values[value]:
15 | to_send[value] = values[value]
16 |
17 | old_values = copy.deepcopy(values)
18 |
19 | if to_send != {}:
20 | manager.broadcast_packet(packet_helper.build(packets.ServerPackets.METER_PARAMETERS_UPDATE, json.dumps(to_send)))
21 |
22 | def fire_initial(token):
23 | manager.send_user_packet(packet_helper.build(packets.ServerPackets.METER_PARAMETERS_UPDATE, json.dumps(old_values)),token)
--------------------------------------------------------------------------------
/server/server_events/server_player_position_parameters_update_event.py:
--------------------------------------------------------------------------------
1 | from server.helpers import packet_helper
2 | from server.connection_manager import manager
3 | from server.constants import packets
4 | import json
5 |
6 | def fire():
7 | positions = {}
8 | for tk in manager.tokens:
9 | position = manager.tokens[tk].position
10 | rotation = manager.tokens[tk].rotation
11 | positions[manager.tokens[tk].username] = {}
12 | positions[manager.tokens[tk].username]["position"] = position
13 | positions[manager.tokens[tk].username]["rotation"] = rotation
14 |
15 | manager.broadcast_packet(packet_helper.build(packets.ServerPackets.PLAYER_POSITION_PARAMETERS_UPDATE, json.dumps(positions)))
16 |
17 | def fire_initial(token):
18 | positions = {}
19 | for tk in manager.tokens:
20 | position = manager.tokens[tk].position
21 | rotation = manager.tokens[tk].rotation
22 | positions[manager.tokens[tk].username] = {}
23 | positions[manager.tokens[tk].username]["position"] = position
24 | positions[manager.tokens[tk].username]["rotation"] = rotation
25 |
26 | manager.send_user_packet(packet_helper.build(packets.ServerPackets.PLAYER_POSITION_PARAMETERS_UPDATE, json.dumps(positions)),token)
--------------------------------------------------------------------------------
/server/server_events/server_recorder_parameters_update_event.py:
--------------------------------------------------------------------------------
1 | from server.helpers import packet_helper
2 | from server.connection_manager import manager
3 | from server.constants import packets
4 | import json
5 | import config
6 |
7 | def fire(recorders):
8 |
9 | recorder_table = {}
10 | for recorder in recorders:
11 | recorder_table[recorder] = {
12 | "channels":recorders[recorder].channels,
13 | "page":recorders[recorder].page,
14 | "elements":recorders[recorder].elements,
15 | "display_on":recorders[recorder].display_on,
16 | }
17 |
18 | manager.broadcast_packet(packet_helper.build(packets.ServerPackets.RECORDER, json.dumps(recorder_table)))
19 |
--------------------------------------------------------------------------------
/server/server_events/server_rod_position_parameters_update_event.py:
--------------------------------------------------------------------------------
1 | from server.helpers import packet_helper
2 | from server.connection_manager import manager
3 | from server.constants import packets
4 | import json
5 | import config
6 | import importlib
7 | old_rods = {}
8 |
9 | def fire(rods): #TODO only send ones that need to be updated (were changed)
10 | new_rods = json.loads(json.dumps(rods)) # hack to copy this properly
11 | #TODO: figure out why the initial rod packet doesnt send right
12 | #NOTE: temporarily commented this part of the code so it will send every rod at all times
13 | for rod_name,rod in rods.items():
14 | if old_rods.get(rod_name) == rod:
15 | del new_rods[rod_name]
16 | else:
17 | old_rods[rod_name] = json.loads(json.dumps(rod)) # hack to copy this properly
18 | #log.debug(f"sent: {rod_name}")
19 |
20 | if new_rods != {}:
21 | manager.broadcast_packet(packet_helper.build(packets.ServerPackets.ROD_POSITION_PARAMETERS_UPDATE, json.dumps(new_rods)))
22 |
23 | # send initial state of indicators
24 | def fire_initial(token):
25 | manager.send_user_packet(packet_helper.build(packets.ServerPackets.ROD_POSITION_PARAMETERS_UPDATE, json.dumps(old_rods)), token)
--------------------------------------------------------------------------------
/server/server_events/server_switch_parameters_update_event.py:
--------------------------------------------------------------------------------
1 | from server.helpers import packet_helper
2 | from server.connection_manager import manager
3 | from server.constants import packets
4 | import json
5 | import config
6 | import importlib
7 | import copy
8 | old_switches = {}
9 | def fire(switches,is_model=False):
10 | model = importlib.import_module(f"simulation.models.{config.config["model"]}.model")
11 | to_send = {}
12 | global old_switches
13 | if is_model == True:
14 | for switch in switches:
15 | if switch in old_switches:
16 | if old_switches[switch] != switches[switch]:
17 | to_send[switch] = {"position" : switches[switch]["position"], "lights" : switches[switch]["lights"], "flag" : switches[switch]["flag"]}
18 |
19 | old_switches = copy.deepcopy(switches)
20 | else:
21 | for switch in switches:
22 | if type(switches[switch]) == int:
23 | to_send[switch] = {"position" : switches[switch], "lights" : model.switches[switch]["lights"], "flag" : model.switches[switch]["flag"]}
24 | else:
25 | to_send[switch] = {"position" : switches[switch]["position"], "lights" : model.switches[switch]["lights"], "flag" : switches[switch]["flag"]}
26 |
27 | if to_send != {}:
28 | manager.broadcast_packet(packet_helper.build(packets.ServerPackets.SWITCH_PARAMETERS_UPDATE, json.dumps(to_send)))
29 |
30 | # used when a client first connects to sync up switch positions
31 | def fire_initial(token):
32 | model = importlib.import_module(f"simulation.models.{config.config["model"]}.model")
33 | switches_to_send = {}
34 | for switch in model.switches:
35 | switches_to_send[switch] = {"position" : model.switches[switch]["position"], "lights" : model.switches[switch]["lights"], "flag" : model.switches[switch]["flag"]}
36 | manager.send_user_packet(packet_helper.build(packets.ServerPackets.SWITCH_PARAMETERS_UPDATE, json.dumps(switches_to_send)), token)
--------------------------------------------------------------------------------
/server/server_events/server_user_logout_event.py:
--------------------------------------------------------------------------------
1 | from server.connection_manager import manager
2 | from server.constants import packets
3 | from server.helpers import packet_helper
4 | import json
5 |
6 | def fire(username):
7 | manager.broadcast_packet(packet_helper.build(packets.ServerPackets.USER_LOGOUT, username))
--------------------------------------------------------------------------------
/server/server_events/server_voip_event.py:
--------------------------------------------------------------------------------
1 | from server.helpers import packet_helper
2 | from server.connection_manager import manager
3 | from server.constants import packets
4 |
5 | def fire(data,player):
6 |
7 | for user in manager.tokens:
8 | manager.send_user_packet(packet_helper.build(packets.ServerPackets.VOIP, "%s|%s" % (data,player)),manager.tokens[user].token)
9 |
--------------------------------------------------------------------------------
/simulation/__init__.py:
--------------------------------------------------------------------------------
1 | from . import init_simulation
--------------------------------------------------------------------------------
/simulation/constants/annunciator_states.py:
--------------------------------------------------------------------------------
1 | from enum import IntEnum
2 |
3 | class AnnunciatorStates(IntEnum):
4 | CLEAR = 0
5 | ACTIVE = 1
6 | ACKNOWLEDGED = 2
7 | ACTIVE_CLEAR = 3
--------------------------------------------------------------------------------
/simulation/constants/electrical_types.py:
--------------------------------------------------------------------------------
1 | from enum import IntEnum
2 |
3 | class ElectricalType(IntEnum):
4 | BREAKER = 0,
5 | TRANSFORMER = 1,
6 | BUS = 2,
7 | SOURCE = 3,
--------------------------------------------------------------------------------
/simulation/constants/equipment_states.py:
--------------------------------------------------------------------------------
1 | from enum import IntEnum
2 |
3 | class EquipmentStates(IntEnum):
4 | RUNNING = 0, #Running/Enabled/Spinning/Energized
5 | STARTING = 1,
6 | STOPPING = 2,
7 | STOPPED = 3,
--------------------------------------------------------------------------------
/simulation/constants/fluid.py:
--------------------------------------------------------------------------------
1 | import math
2 |
3 | class LiquidHeader:
4 | def __init__(self,fill,normal_pressure,diameter,length=2000):
5 | """diameter and length in mm"""
6 | self.fill = fill
7 | self.normal_pressure = normal_pressure #PSIG
8 | self.feeders = []
9 |
10 | volume = diameter / 2
11 | volume = math.pi * (volume ** 2)
12 | volume = volume * length
13 | volume = volume / 1e6 # convert liters
14 |
15 | self.liters_at_full = volume
16 |
17 | def add_feeder(self,header,valve=None):
18 | self.feeders.append({"header":header,"valve":valve})
19 |
20 | def get_pressure(self):
21 | return self.fill * self.normal_pressure
22 |
23 | def calculate(self):
24 | Press = self.fill * self.normal_pressure
--------------------------------------------------------------------------------
/simulation/constants/pump.py:
--------------------------------------------------------------------------------
1 | from simulation.models.control_room_columbia.general_physics import ac_power
2 | from simulation.models.control_room_columbia.general_physics import fluid
3 | import math
4 | import log
5 |
6 | def clamp(val, clamp_min, clamp_max):
7 | return min(max(val,clamp_min),clamp_max)
8 |
9 |
10 | class MotorPump():
11 | def __init__(self,name="",motor_breaker_closed=False,bus="",horsepower=0,rated_rpm=1800,rated_discharge_press=0,rated_flow=0,current_limit=0,header="",suct_header="",loop_seq=False,custom=False):
12 | self.name = name
13 | self.motor_breaker_closed = motor_breaker_closed
14 | self.bus = bus
15 | self.horsepower = horsepower
16 | self.rated_rpm = rated_rpm
17 | self.rated_discharge_press = rated_discharge_press
18 | self.rated_flow = rated_flow
19 | self.current_limit = current_limit
20 | self.header = header
21 | self.suct_header = suct_header
22 | self.loop_seq = loop_seq
23 | self.custom = custom
24 |
25 | def start(self):
26 | self.motor_breaker_closed = True
27 |
28 | def stop(self):
29 | self.motor_breaker_closed = False
30 |
31 | def calculate_suction(self,delta):
32 |
33 | if self.suct_header== "":
34 | return self.flow
35 | #a
36 | #if pump["npshr"] < fluid.headers[pump["suct_header"]]["pressure"]*2.3072493927233:
37 | return self.flow
38 |
39 | return 0
40 |
41 | def calculate(self,delta):
42 | #undervoltage breaker trip
43 |
44 | voltage = 0
45 | #TODO: use the source classes in the valve stuff
46 | try:
47 | pump_bus = ac_power.busses[self.bus]
48 |
49 | voltage = pump_bus.voltage_at_bus()
50 |
51 | if voltage < 120 and self.motor_breaker_closed and not self.custom:
52 | self.motor_breaker_closed = False
53 | self.was_closed = True
54 |
55 |
56 | if not self.name in pump_bus.info["loads"]:
57 | pump_bus.register_load(self.watts,self.name )
58 | else:
59 | pump_bus.modify_load(self.watts,self.name )
60 | except:
61 | #log.warning("Pump does not have an available bus!")
62 | voltage = 4160
63 |
64 | if not self.custom:
65 | if self.loop_avail and self.was_closed:
66 | self.motor_breaker_closed = True
67 | self.was_closed = False
68 |
69 | target = 0
70 |
71 | if self.motor_breaker_closed:
72 | target = ((self.rated_rpm*(pump_bus.info["frequency"]/60))-self.rpm)
73 | else:
74 | target = -self.rpm
75 |
76 | Acceleration = (target)*1*delta
77 |
78 | self.rpm = clamp(self.rpm+Acceleration,0,self.rated_rpm+100)
79 |
80 | self.flow = self.rated_flow*(self.rpm/self.rated_rpm)
81 | self.flow = self.calculate_suction(delta)
82 | self.discharge_pressure = self.rated_discharge_press*(self.rpm/self.rated_rpm)
83 |
84 | if self.header != "":
85 | self.actual_flow = fluid.inject_to_header(self.flow,self.discharge_pressure,fluid.headers[self.header]["temperature"],self.header,self.suct_header)
86 | else:
87 | self.actual_flow = self.flow
88 |
89 | #amps
90 | if voltage > 0 and self.motor_breaker_closed:
91 | AmpsFLA = (self.horsepower*746)/((math.sqrt(3)*voltage*0.876*0.95)) #TODO: variable motor efficiency and power factor
92 |
93 | self.amperes = (AmpsFLA*clamp(self.actual_flow/self.rated_flow,0.48,1))+(AmpsFLA*5*(Acceleration/(self.rated_rpm*1*delta)))
94 | self.watts = voltage*self.amperes*math.sqrt(3)
95 | else:
96 | self.amperes = 0
97 | self.watts = 0
98 |
99 | class ShaftDrivenPump():
100 | def __init__(self,rated_rpm,rated_discharge_press,rated_flow,header,suct_header):
101 | self.rated_rpm = rated_rpm
102 | self.rated_discharge_press =rated_discharge_press
103 | self.rated_flow = rated_flow
104 | self.header = header
105 | self.suct_header = suct_header
106 | self.rpm = 0
107 |
108 | def set_rpm(self,rpm):
109 | self.rpm = rpm
110 |
111 | def calculate_suction(self,delta):
112 |
113 | if self.suct_header== "":
114 | return self.flow
115 | #a
116 | #if pump["npshr"] < fluid.headers[pump["suct_header"]]["pressure"]*2.3072493927233:
117 | return self.flow
118 |
119 | return 0
120 |
121 | def calculate(self,delta):
122 |
123 | self.flow = self.rated_flow*(self.rpm/self.rated_rpm)
124 | self.flow = self.calculate_suction(delta)
125 | self.discharge_pressure = self.rated_discharge_press*(self.rpm/self.rated_rpm)
126 |
127 | self.actual_flow = fluid.inject_to_header(self.flow,self.discharge_pressure,fluid.headers[self.header]["temperature"],self.header,self.suct_header)
128 |
129 |
130 |
--------------------------------------------------------------------------------
/simulation/init_simulation.py:
--------------------------------------------------------------------------------
1 | import time
2 | import math
3 | import importlib
4 | from server.server_events import server_meter_parameters_update_event
5 | from server.server_events import server_indicator_parameters_update_event
6 | from server.server_events import server_switch_parameters_update_event
7 | from server.server_events import server_alarm_parameters_update_event
8 | from server.server_events import server_recorder_parameters_update_event
9 | import log
10 | import config
11 | from threading import Thread
12 |
13 | # import the model specified in the config
14 | model = importlib.import_module(f"simulation.models.{config.config["model"]}.model")
15 |
16 | class Simulation:
17 | def __init__(self):
18 | self.timestep = 0.1 # time between model steps
19 | self.default_timestep = 0.1 # what is 1x speed
20 | self.minimum_speedup_drop = 2 # skip sending packets if we exceed this many times 1x speed
21 | self.timesteps = 0 # how many timesteps did we take
22 | self.prev_delta = self.timestep
23 | self.simulation_normal = Thread(target=self.timer)
24 | self.simulation_normal.start()
25 |
26 | self.fast_timestep = 1/20 # time between model steps
27 | self.fast_timesteps = 0 # how many timesteps did we take
28 | self.fast_prev_delta = self.fast_timestep
29 | self.simulation_fast = Thread(target=self.timer_fast)
30 | self.simulation_fast.start()
31 |
32 | def timer(self):
33 | while True:
34 | start = time.perf_counter()
35 | model.model_run(self.prev_delta)
36 | drop = 0
37 | if (self.default_timestep/self.timestep) >= self.minimum_speedup_drop:
38 | drop = self.timesteps % math.floor((self.default_timestep/self.timestep)/self.minimum_speedup_drop)
39 | if drop == 0: # prevent flooding clients on high speedups
40 | server_meter_parameters_update_event.fire(model.values)
41 | server_indicator_parameters_update_event.fire(model.indicators)
42 | server_switch_parameters_update_event.fire(model.switches,True)
43 | server_alarm_parameters_update_event.fire(model.alarms)
44 | server_recorder_parameters_update_event.fire(model.recorders)
45 | end = time.perf_counter()
46 | delta = end - start
47 | if self.timestep - delta < 0:
48 | log.warning(f"[ALERT] simulation cant keep up, time we have:{self.timestep}, time we took: {delta}")
49 | else:
50 | time.sleep(self.timestep - delta)
51 | self.timesteps += 1
52 | self.prev_delta = max(self.timestep, delta)
53 |
54 | def timer_fast(self):
55 | while True:
56 | start = time.perf_counter()
57 | model.model_run_fast(self.fast_prev_delta)
58 | end = time.perf_counter()
59 | delta = end - start
60 |
61 | if self.fast_timestep - delta < 0:
62 | log.warning(f"[ALERT] fast simulation cant keep up, time we have:{self.fast_timestep}, time we took: {delta}")
63 | else:
64 | time.sleep(self.fast_timestep - delta)
65 |
66 | self.fast_prev_delta = max(self.fast_timestep, delta)
67 |
68 | simulation = Simulation()
69 |
--------------------------------------------------------------------------------
/simulation/models/control_room_columbia/annunciators.py:
--------------------------------------------------------------------------------
1 | from simulation.constants.annunciator_states import AnnunciatorStates
2 | from simulation.models.control_room_columbia import model
3 | import math
4 |
5 | def bisi(annunciator,name):
6 | if annunciator["alarm"]:
7 | assert name in model.buttons, "No pushbutton associated with "+name+" BISI!"
8 | if annunciator["state"] == AnnunciatorStates.CLEAR:
9 | annunciator["state"] = AnnunciatorStates.ACTIVE
10 |
11 | if model.buttons[name]["state"]:
12 | if annunciator["state"] == AnnunciatorStates.ACTIVE:
13 | annunciator["state"] = AnnunciatorStates.ACKNOWLEDGED
14 |
15 | else:
16 | annunciator["state"] = AnnunciatorStates.CLEAR
17 |
18 | def run():
19 |
20 | for alarm in model.alarms:
21 | annunciator = model.alarms[alarm]
22 | group = annunciator["group"]
23 |
24 | if group == "-1": #this is a BISI
25 | bisi(annunciator,alarm)
26 | continue
27 |
28 | #reflash + active normally
29 | if annunciator["alarm"] and (annunciator["state"] == AnnunciatorStates.CLEAR or annunciator["state"] == AnnunciatorStates.ACTIVE_CLEAR):
30 | annunciator["state"] = AnnunciatorStates.ACTIVE
31 |
32 | if not annunciator["alarm"] and annunciator["state"] == AnnunciatorStates.ACKNOWLEDGED:
33 | annunciator["state"] = AnnunciatorStates.ACTIVE_CLEAR
34 |
35 | #acknowledge behavior
36 | if annunciator["state"] == AnnunciatorStates.ACTIVE:
37 | for button in model.buttons:
38 | if "ALARM_ACK" in button and group in button and model.buttons[button]["state"]:
39 | annunciator["state"] = AnnunciatorStates.ACKNOWLEDGED
40 | annunciator["silenced"] = False
41 |
42 | #clear behavior
43 | if annunciator["state"] == AnnunciatorStates.ACTIVE_CLEAR:
44 | for button in model.buttons:
45 | if "ALARM_RESET" in button and group in button and model.buttons[button]["state"]:
46 | annunciator["state"] = AnnunciatorStates.CLEAR
47 | annunciator["silenced"] = False
48 |
49 | #silence behavior
50 | if annunciator["state"] == AnnunciatorStates.ACTIVE or annunciator["state"] == AnnunciatorStates.ACTIVE_CLEAR:
51 | for button in model.buttons:
52 | if "ALARM_SILENCE" in button and group in button and model.buttons[button]["state"]:
53 | annunciator["silenced"] = True
54 |
55 | #at columbia, pressing any main panel acknowledge will silence all alarms for other main panels.
56 | #this was done because there was not enough room for silence pushbuttons (and they already laid out the panels)
57 | for button in model.buttons:
58 | #TODO: blacklist/whitelist groups together
59 | if "ALARM_ACK" in button and model.buttons[button]["state"]:
60 | annunciator["silenced"] = True
61 |
62 | #unsilence if the alarm comes back
63 |
64 | if annunciator["silenced"] and not annunciator["alarm"]:
65 | annunciator["silenced"] = False
66 |
67 |
68 | annunciator["alarm"] = False
69 |
--------------------------------------------------------------------------------
/simulation/models/control_room_columbia/general_physics/accidents.py:
--------------------------------------------------------------------------------
1 | from simulation.models.control_room_columbia.reactor_physics import pressure
2 | from simulation.models.control_room_columbia.reactor_physics import reactor_inventory
3 | from simulation.models.control_room_columbia.reactor_physics import reactor_physics
4 | from simulation.models.control_room_columbia.general_physics import tank
5 | import math
6 |
7 | steamline_break = False
8 |
9 | def run(delta):
10 |
11 | global steamline_break
12 |
13 | if steamline_break: #un-hardcode later
14 | steamline_pressure = pressure.Pressures["Vessel"]
15 | drywell_pressure = pressure.Pressures["Drywell"]
16 |
17 | break_size = 500 #mm
18 | break_length = 2000 #mm dont change
19 |
20 | break_size = break_size/2 #to radius
21 | break_size = break_size * 0.1 # to cm
22 |
23 | flow_resistance = (8 * 33 * break_length)/(math.pi*(break_size**4))
24 |
25 | flow = (steamline_pressure-drywell_pressure)/flow_resistance
26 |
27 | flow = flow/1000 # to l/s
28 | flow = flow * delta
29 |
30 | reactor_physics.kgSteamDrywell += flow
31 |
32 | pressure.Pressures["Drywell"] = pressure.PartialPressure(pressure.GasTypes["Steam"],reactor_physics.kgSteamDrywell,60,pressure.Volumes["Drywell"])
33 |
34 | reactor_inventory.remove_steam(flow)
35 |
36 | print(pressure.Pressures["Drywell"]/6895)
37 |
38 | #downcomers
39 |
40 | #there are 88 downcomers
41 | #each 23.25" diameter
42 |
43 | #exhaust 8ft below min level
44 | val = False
45 |
46 | if val:
47 | steamline_break = True
48 |
49 | downcomer_diameter = 23.25*25.4 #to mm from inches
50 |
51 | supp_pool_level = tank.tanks["Wetwell"].get_level()
52 | supp_pool_level = supp_pool_level/304.8 #to feet from mm
53 |
54 | #calculate the level of the supp pool vs the downcomer exits
55 |
56 | downcomer_level = 10 #maybe correct?
57 |
58 | felt_water_column = max(supp_pool_level - downcomer_level,0)
59 | water_pressure = felt_water_column * 2989.067 #ft of h2o to pascal
60 | total_pressure = water_pressure + pressure.Pressures["Wetwell"]
61 |
62 | drywell_pressure = pressure.Pressures["Drywell"]
63 |
64 | downcomer_r = downcomer_diameter/2
65 | downcomer_r = downcomer_r *0.1 # to cm
66 |
67 | downcomer_length = 2000 #dont change
68 |
69 | flow_resistance = (8 * 33 * downcomer_length)/(math.pi*(downcomer_r**4))
70 |
71 | flow = (drywell_pressure-total_pressure)/flow_resistance
72 |
73 | flow = flow/1000 # to l/s
74 | flow = flow * delta
75 |
76 | if flow > 0 and reactor_physics.kgSteamDrywell-flow > 0:
77 | #cant reverse flow
78 | reactor_physics.kgSteamDrywell -= flow
79 | tank.tanks["Wetwell"].add_water(flow) #100% efficiency for now
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
--------------------------------------------------------------------------------
/simulation/models/control_room_columbia/general_physics/air_system.py:
--------------------------------------------------------------------------------
1 | from simulation.models.control_room_columbia import model
2 |
3 | from enum import IntEnum
4 |
5 | class FailureModes(IntEnum):
6 | AS_IS = 0,
7 | CLOSED = 1,
8 | OPEN = 2,
9 |
10 | class SupplyAir():
11 | def __init__(self,header,failure_pressure,failure_mode):
12 | self.header = header
13 | self.failure_pressure = failure_pressure
14 | self.failure_mode = failure_mode
15 |
16 | def check_available(self):
17 | return self.header.fill * self.header.normal_pressure > self.failure_pressure
18 |
19 | class SupplyElectric():
20 | def __init__(self,bus,failure_voltage,failure_mode):
21 | self.bus = bus
22 | self.failure_voltage = failure_voltage
23 | self.failure_mode = failure_mode
24 |
25 | def check_available(self):
26 | return self.bus.is_voltage_at_bus(self.failure_voltage)
27 |
28 | class PressureControlValve:
29 | def __init__(self,percent_open,header,normal_pressure,band=10):
30 | self.percent_open = percent_open
31 | self.header = header
32 | self.normal_pressure = normal_pressure
33 | self.band = band
34 |
35 | def calculate(self):
36 | self.percent_open = min(max(((self.normal_pressure-self.header.get_pressure())/self.band)*100,0),100)
37 |
38 | class Valve:
39 | def __init__(self,percent_open,switch_name=None,seal_in=False,sealed_in=False,open_speed=0,supply = None,drop_indication_on_motive_lost = False, only_indicate = False,):
40 | self.percent_open = percent_open
41 | self.switch_name = switch_name
42 | self.seal_in = seal_in
43 | self.sealed_in = sealed_in
44 | self.open_speed = open_speed*0.1
45 | self.supply = supply
46 | self.drop_indication_on_motive_lost = drop_indication_on_motive_lost
47 | self.only_indicate = only_indicate
48 |
49 | def stroke_open(self,speed=None):
50 | if speed == None:
51 | speed = self.open_speed
52 |
53 | self.percent_open = min(max(self.percent_open+speed,0),100)
54 |
55 | def stroke_closed(self,speed=None):
56 | if speed == None:
57 | speed = self.open_speed
58 |
59 | self.percent_open = min(max(self.percent_open-speed,0),100)
60 |
61 | def close(self):
62 | self.sealed_in = False
63 |
64 | def open(self):
65 | self.sealed_in = True
66 |
67 | def get_switch_position(self):
68 | if self.switch_name not in model.switches:
69 | return False
70 |
71 | return model.switches[self.switch_name]["position"]
72 |
73 | def calculate(self):
74 | if self.switch_name != None:
75 | switch = model.switches[self.switch_name]
76 |
77 | available = True
78 | if self.supply != None:
79 | available = self.supply.check_available()
80 | if available == False:
81 | match self.supply.failure_mode:
82 | case FailureModes.CLOSED:
83 | self.stroke_closed()
84 | case FailureModes.OPEN:
85 | self.stroke_open()
86 |
87 | if switch["lights"] != {}:
88 | switch["lights"]["green"] = self.percent_open < 100
89 | switch["lights"]["red"] = self.percent_open > 0
90 |
91 | if available == False or self.only_indicate:
92 | return
93 |
94 | if not self.seal_in:
95 | if switch["position"] == 2:
96 | self.stroke_open()
97 | elif switch["position"] == 0:
98 | self.stroke_closed()
99 | else:
100 | if len(switch["positions"]) < 3:
101 | if switch["position"] == 1:
102 | self.sealed_in = True
103 | elif switch["position"] == 0:
104 | self.sealed_in = False
105 | else:
106 | if switch["position"] == 2:
107 | self.sealed_in = True
108 | elif switch["position"] == 0:
109 | self.sealed_in = False
110 |
111 | if self.sealed_in:
112 | self.stroke_open()
113 | else:
114 | self.stroke_closed()
115 |
116 | class AirHeader:
117 | def __init__(self,fill,normal_pressure,size=1):
118 | self.fill = fill
119 | self.normal_pressure = normal_pressure #PSIG
120 | self.size=size #allows making some headers bigger than others, like tanks and whatnot
121 | self.feeders = []
122 |
123 | def add_feeder(self,header,valve=None,isolation_valve=None):
124 | self.feeders.append({"header":header,"valve":valve,"isolation":isolation_valve,})
125 |
126 | def get_pressure(self):
127 | return self.fill * self.normal_pressure
128 |
129 | def calculate(self):
130 | Press = self.fill * self.normal_pressure
131 | Total_Flow = 0
132 |
133 |
134 | for feed in self.feeders:
135 | DeltaP = (feed["header"].fill * feed["header"].normal_pressure) - Press
136 |
137 | Flow = DeltaP/self.normal_pressure
138 | Flow = Flow*0.0035
139 |
140 | if feed["valve"] != None:
141 | Flow = Flow*feed["valve"].percent_open
142 |
143 | if feed["isolation"] != None:
144 | Flow = Flow*(feed["isolation"].percent_open/100)
145 |
146 | Total_Flow += Flow
147 |
148 | feed["header"].fill -= ((Flow*self.normal_pressure)/feed["header"].normal_pressure)/feed["header"].size#/len(self.feeders)
149 |
150 | if len(self.feeders) == 0:
151 | return
152 |
153 | #Total_Flow /= len(self.feeders)
154 |
155 | self.fill += Total_Flow/self.size
156 |
157 | class Vent:
158 | def __init__(self):
159 | self.fill = 1
160 | self.normal_pressure = 14.7 #PSIG
161 | self.size = 1
162 | self.feeders = []
163 |
164 | def add_feeder(self,header,valve=None):
165 | self.feeders.append({"header":header,"valve":valve})
166 |
167 | def get_pressure(self):
168 | return self.fill * self.normal_pressure
169 |
170 | def calculate(self):
171 | Press = self.fill * self.normal_pressure
172 |
173 |
174 | for feed in self.feeders:
175 | DeltaP = (feed["header"].fill * feed["header"].normal_pressure) - Press
176 |
177 | Flow = DeltaP/self.normal_pressure
178 | Flow = Flow*0.001
179 |
180 | if feed["valve"] != None:
181 | Flow = Flow*feed["valve"].percent_open
182 |
183 | feed["header"].fill -= ((Flow*self.normal_pressure)/feed["header"].normal_pressure)#/len(self.feeders)
184 |
185 | class Compressor():
186 | def __init__(self,discharge_pressure,supply,horsepower):
187 | self.fill = 1
188 | self.size = 1
189 | self.normal_pressure = discharge_pressure #these emulate a header, so there is less code copied around
190 |
191 | self.motor_breaker_closed = False
192 | #assert supply in SupplyElectric, "You cant use air to move air wtf"
193 | self.supply = supply
194 | self.horespower = horsepower #used to calculate LRA and FLA
195 |
196 | self.has_loading_system = False
197 | self.band_low = 0
198 | self.band_high = 0
199 | self.unloaded = False
200 | self.pressure_reference = None
201 |
202 | def stop(self):
203 | self.motor_breaker_closed = False
204 |
205 | def start(self):
206 | self.motor_breaker_closed = True
207 |
208 | def add_loading_system(self,band_low,band_high,pressure_reference = None):
209 | self.has_loading_system = True
210 | self.band_low = band_low
211 | self.band_high = band_high
212 |
213 | if pressure_reference == None:
214 | pressure_reference = self
215 |
216 | self.pressure_reference = pressure_reference
217 |
218 | def loading_system(self):
219 | #Simple P controller
220 | Band = self.band_high - self.band_low
221 | Press = self.pressure_reference.fill * self.pressure_reference.normal_pressure
222 | Demand = min(max((self.band_high - Press) / Band,0),1)
223 | Demand *= 100
224 |
225 | self.unloaded = Demand <= 25 #i dont really know what this is supposed to be at
226 | return min(max(Demand,0),100)
227 |
228 | def calculate(self):
229 | self.fill = max(self.fill,0)
230 | if self.motor_breaker_closed and self.supply.check_available():
231 | accel = 0.05
232 |
233 | if self.has_loading_system:
234 | accel = accel * (self.loading_system()/100)
235 |
236 | self.fill += (1-self.fill)*accel
237 | else:
238 | self.unloaded = False
239 |
240 |
241 |
242 |
243 |
244 |
245 |
--------------------------------------------------------------------------------
/simulation/models/control_room_columbia/general_physics/dc_power.py:
--------------------------------------------------------------------------------
1 | def findAmpHoursToZero(amphours=1190,start_voltage=130,end_voltage=105):
2 | return (start_voltage/(start_voltage-end_voltage))*amphours #Not sure how accurate this is but it should be close enough for us
3 |
4 | class Battery:
5 | def __init__(self,voltage,amphours):
6 | self.normal_amphours = amphours
7 | self.amphours = amphours
8 | self.voltage = voltage
9 | self.normal_voltage = voltage
10 |
11 | def whoami(self):
12 | return self.__class__
13 |
14 | def SetCharge(self,percent):
15 | self.amphours = self.normal_amphours*(percent/100)
16 |
17 | def calculate(self):
18 |
19 | amps = 1000 #This should work for negative numbers, which is effectively charging the battery.
20 |
21 | amp_hours_used = (amps*0.1) #Delta time.
22 |
23 | amp_hours_used = (amps/36000) #To hours
24 |
25 | self.amphours = self.amphours-amp_hours_used
26 |
27 | self.voltage = (self.amphours/self.normal_amphours)*self.normal_voltage
28 |
29 | class Bus:
30 | def __init__(self,voltage):
31 | self.voltage = voltage
32 | self.normal_voltage = voltage
33 | self.loads = {}
34 | self.batteries = []
35 | self.feeders = []
36 | self.current = 0
37 |
38 | def whoami(self):
39 | return self.__class__
40 |
41 | def AddBattery(self,battery):
42 | self.batteries.append(battery)
43 |
44 | def AddFeeder(self,feeder):
45 | self.feeders.append(feeder)
46 |
47 | def AddLoad(self,load,name):
48 | if not name in self.loads:
49 | self.loads[name] = load
50 |
51 | def ModifyLoad(self,load,name):
52 | if name in self.loads:
53 | self.loads[name] = load
54 |
55 | def RemoveLoad(self,name):
56 | if name in self.loads:
57 | self.loads.pop(name)
58 |
59 | def calculate(self):
60 | a = 0
61 |
62 | class Fuse:
63 | def __init__(self,disconnect_current):
64 | self.disconnect_current = disconnect_current
65 | self.current = 0
66 | self.running = None
67 | self.incoming = None
68 | self.closed = True
69 |
70 | def whoami(self):
71 | return self.__class__
72 |
73 | def SetIncoming(self,incoming):
74 | self.incoming = incoming
75 |
76 | def SetRunning(self,running):
77 | self.running = running
78 |
79 | def Repair(self):
80 | self.closed = True
81 |
82 | def calculate(self):
83 | if self.current >= self.disconnect_current:
84 | self.closed = False
85 |
86 | class Breaker:
87 | def __init__(self):
88 | self.current = 0
89 | self.running = None
90 | self.incoming = None
91 | self.closed = True
92 |
93 | def whoami(self):
94 | return self.__class__
95 |
96 | def SetIncoming(self,incoming):
97 | self.incoming = incoming
98 |
99 | def SetRunning(self,running):
100 | self.running = running
101 |
102 | def calculate(self):
103 | a = 0
104 |
--------------------------------------------------------------------------------
/simulation/models/control_room_columbia/general_physics/gas.py:
--------------------------------------------------------------------------------
1 | from simulation.models.control_room_columbia import model
2 | from simulation.models.control_room_columbia.general_physics import fluid
3 | from simulation.models.control_room_columbia.reactor_physics import pressure
4 | import math
5 |
6 |
7 | def clamp(val, clamp_min, clamp_max):
8 | return min(max(val,clamp_min),clamp_max)
9 |
10 | def calculate_header_pressure(header_name:str):
11 |
12 | header = fluid.headers[header_name]
13 |
14 | from simulation.models.control_room_columbia.reactor_physics import pressure
15 | header_press = pressure.PartialPressure(pressure.GasTypes["Steam"],header["mass"],60,header["volume"])
16 | header["pressure"] = header_press
17 |
18 | def run(delta):
19 |
20 | for valve_name in fluid.valves:
21 | valve = fluid.valves[valve_name]
22 |
23 | if valve["input"] == None or valve["output"] == None:
24 | continue
25 |
26 | inlet = fluid.get_header(valve["input"])
27 | if valve["output"] == "magic":
28 | outlet = {"pressure": 0,"mass" : 0,"type" : fluid.FluidTypes.Gas}
29 | else:
30 | outlet = fluid.get_header(valve["output"])
31 |
32 | if inlet["type"] == fluid.FluidTypes.Liquid or outlet["type"] == fluid.FluidTypes.Liquid:
33 | continue #this is handled by fluid.py
34 |
35 | #gas has to be calculated differently
36 | #It is the exact same as regular fluids, however it has an extra factor, (P1+P2/2*P2), expressing the average pressure relative to the outlet pressure
37 |
38 | radius = valve["diameter"]/2
39 | radius = radius*0.1 #to cm
40 |
41 | flow_resistance = (8*3.3*10000)/(math.pi*(radius**4))
42 |
43 | flow = (inlet["pressure"]-max(outlet["pressure"],0))/flow_resistance
44 |
45 | #here is our extra factor
46 | #if max(outlet["pressure"],0) != 0:
47 | #flow = (inlet["pressure"]+max(outlet["pressure"],0))/(2*(max(outlet["pressure"],0)))
48 |
49 | flow = abs(flow)
50 |
51 | flow = flow*(valve["percent_open"]/100)
52 | #flow is in cubic centimeters per second
53 | flow = flow/1000 #to liter/s
54 |
55 | flow = flow*delta #to liter/0.1s (or the sim time)
56 |
57 | if inlet["pressure"] < outlet["pressure"]:
58 | valve["flow"] = 0
59 | continue
60 | else:
61 | fluid.valve_inject_to_header(flow*-1,0,valve["input"],valve_name)
62 | if valve["output"] != "magic":
63 | fluid.valve_inject_to_header(flow,0,valve["output"],valve_name)
64 |
65 | valve["flow"] = flow
66 |
67 | #print(fluid.headers["bypass_steam_header"]["pressure"]/6895)
--------------------------------------------------------------------------------
/simulation/models/control_room_columbia/general_physics/main_condenser.py:
--------------------------------------------------------------------------------
1 | #import iapws
2 | from simulation.models.control_room_columbia.reactor_physics import pressure
3 | from simulation.models.control_room_columbia import model
4 |
5 | MainCondenserPressure = 0 #pa
6 | MainCondenserVolume = 928500.26 #liters #TODO: need actual size
7 | MainCondenserAtmosphere = {
8 | "Nitrogen" : 580,#850, #kg
9 | "Oxygen" : 0,
10 | "Hydrogen" : 0,
11 | }
12 | MainCondenserHotwellMass = 308521 #about half full?
13 | MainCondenserBackPressure = 15
14 | MainCondenserBackPressureA = 0
15 |
16 | def initalize():
17 | MainCondenserPressure = pressure.PartialPressure(pressure.GasTypes["Nitrogen"],MainCondenserAtmosphere["Nitrogen"],100,MainCondenserVolume)
18 | #print(MainCondenserPressure/6895) #PSI
19 | #print(MainCondenserPressure/3386) #In.Hg
20 | #print(abs((MainCondenserPressure/3386)-29.9212)) #In.Hg, backpressure
21 |
22 | def run():
23 | #Condensation is:
24 | #Heat removed from steam (BTU/hr)
25 | #Latent heat of steam (BTU/lb)
26 |
27 | #Output is water in lbs/hr
28 |
29 |
30 |
31 |
32 | """CirculatingWater = 0 #gpm
33 | CirculatingWater = CirculatingWater*500.4 #(500.4 lb/hr per GPM), so this is lb/hr now
34 | #Specific heat of water is 1btu/lb/degF
35 | #assume a 40 degree difference in the water (Calculate this at some point) at inlet vs outlet
36 | DeltaT = 40
37 | BTUHRs = CirculatingWater*DeltaT
38 |
39 | #Condensation
40 |
41 | data = getRegion1Data(300,9481.877072) #uhhh i dont know, probably like 300c at 1.37523 psi (2.8 inHg)
42 |
43 | LatentHeat = data["h"]/2.326 #2.326 for btu/lb
44 |
45 | Condensate = BTUHRs/LatentHeat
46 |
47 | Condensate = Condensate/120 #to seconds
48 | Condensate = Condensate*0.1 #deltatime
49 |
50 | print(Condensate)""" #im going to cry myself to sleep tonight guys
51 |
52 | #Steam Jet Air Ejectors are used because of;
53 | #air in-leakage from packing and gasket leaks
54 | #radiolytic decomposition of water into h2 and o2
55 |
56 | #when not at power the Condenser Air Removal pumps are used (two 50% capacity)
57 | #CAR is not filtered through offgas, and is alarmed for high radiation at its output
58 |
59 | RxPress = pressure.Pressures["Vessel"] #TODO: Place this after the Main Steam Isolation Valves!!!!
60 |
61 | MainCondenserPressure = pressure.PartialPressure(pressure.GasTypes["Nitrogen"],MainCondenserAtmosphere["Nitrogen"],100,MainCondenserVolume)
62 |
63 | #print(abs((MainCondenserPressure/3386)-29.9212)) #In.Hg vacuum
64 |
65 | global MainCondenserBackPressure
66 | global MainCondenserBackPressureA
67 |
68 | MainCondenserBackPressure = abs((MainCondenserPressure/3386)-29.9212)
69 | MainCondenserBackPressureA = abs((MainCondenserPressure/3386))
70 |
71 | #Pretend theres some amount of in-leakage
72 | Atmospheres = MainCondenserPressure/101325
73 |
74 | if Atmospheres < 1:
75 | MainCondenserAtmosphere["Nitrogen"] += 100*abs(Atmospheres-1)*0.1 #Assume 500kg/s of in-leakage at 0 atm(randomize this later?)
76 | #TODO: The rated limit is 50 scfm. That is 1.7 kg/s of in-leakage.
77 |
78 | #TODO: Radiolytic decomposition of water, seperation of H2O into two hydrogens and one oxygen from radiation
79 |
80 | #Condenser Air Removal Pumps
81 |
82 | if MainCondenserPressure/3386 <= 5: #CAR can only pull around 25 in.hg and will trip at 25 in.hg vacuum (5 in.hg absolute)
83 | model.pumps["ar_p_1a"]["motor_breaker_closed"] = False
84 | model.pumps["ar_p_1b"]["motor_breaker_closed"] = False
85 |
86 | Car1RPM = model.pumps["ar_p_1a"]["rpm"]
87 | Car2RPM = model.pumps["ar_p_1b"]["rpm"]
88 |
89 | Car1Flow = (Car1RPM/1800)*41 #very very very bad very simplify
90 | Car2Flow = (Car2RPM/1800)*41
91 |
92 | TotalAirRemoved = Car1Flow+Car2Flow
93 | TotalAirRemoved *= 0.1 #Deltatime
94 |
95 | #Discharged to the Turbine Building
96 |
97 | MainCondenserAtmosphere["Nitrogen"] -= TotalAirRemoved
98 |
99 | #print(MainCondenserHotwellMass)
100 |
101 | #Steam Jet Air Ejectors
102 |
103 | #Assume for now we are using the aux boilers to run the SJAEs
104 |
105 |
106 |
107 | #MainCondenserAtmosphere["Nitrogen"] -= TotalAirRemovedSJAE
108 |
109 | #Hotwell storage capacity is 163,000 gal
110 |
111 |
112 | def addHotwellWater(kg):
113 | global MainCondenserHotwellMass
114 | MainCondenserHotwellMass+=kg
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
--------------------------------------------------------------------------------
/simulation/models/control_room_columbia/general_physics/main_generator.py:
--------------------------------------------------------------------------------
1 | from simulation.models.control_room_columbia import model
2 | from simulation.models.control_room_columbia.general_physics import main_turbine
3 | from simulation.models.control_room_columbia.general_physics import ac_power
4 | import math
5 | import log
6 |
7 | Generator = {
8 | "Synchronized" : False,
9 | "Output" : 0,
10 | }
11 |
12 |
13 |
14 | def getVoltageAmperesPower():
15 | Voltage = (76.08338608)*main_turbine.Turbine["AngularVelocity"]*math.sqrt(3)
16 | Amperes = Voltage/8
17 |
18 | ReactivePower = 0
19 |
20 | ApparentPower = Voltage*Amperes
21 | ApparentPower = max(ApparentPower,0.1)
22 | KineticEnergy = (1/2) * main_turbine.Turbine["Inertia"] * main_turbine.Turbine["AngularVelocity"]/2
23 |
24 | RealPower = ((main_turbine.Turbine["Torque"]/3)**2)+(KineticEnergy)
25 | PowerFactor = RealPower/ApparentPower
26 | Watts = max(0, Voltage*Amperes*math.sqrt(3*PowerFactor))
27 |
28 | return Voltage, Amperes, Watts, PowerFactor
29 |
30 | def run():
31 |
32 | #TODO: Volts per hertz
33 |
34 | Generator["Synchronized"] = ac_power.breakers["cb_4885"].info["closed"] or ac_power.breakers["cb_4888"].info["closed"]
35 |
36 | Volt, Amp, Power, Factor = getVoltageAmperesPower()
37 |
38 |
39 | if Generator["Synchronized"]:
40 | Generator["Output"] = Power
41 | else:
42 | Generator["Output"] = 0
43 |
44 | ac_power.sources["GEN"].info["voltage"] = Volt
45 | ac_power.sources["GEN"].info["frequency"] = (main_turbine.Turbine["AngularVelocity"]/(math.pi))
46 |
47 | if Generator["Synchronized"] and (abs(ac_power.sources["ASHE500"].info["phase"]-ac_power.busses["gen_bus"].info["phase"]) > 10):
48 | #basically, when something is synchronized out of phase,
49 | #the bigger generator wins and forces the smaller one into phase,
50 | #violently...
51 | #in our case the grid always wins and the generator becomes a new showpiece in our parking lot
52 | #TODO: send the fucking turbine
53 | log.info("holy shit bro")
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
--------------------------------------------------------------------------------
/simulation/models/control_room_columbia/general_physics/main_turbine.py:
--------------------------------------------------------------------------------
1 | from simulation.models.control_room_columbia import model
2 | from simulation.models.control_room_columbia.reactor_physics import pressure
3 | from simulation.models.control_room_columbia.reactor_physics import steam_functions
4 | from simulation.models.control_room_columbia.general_physics import fluid
5 | from simulation.models.control_room_columbia.general_physics import main_generator
6 | from simulation.models.control_room_columbia.general_physics import main_condenser
7 | import math
8 |
9 | Turbine = {
10 | "RPM": 0,
11 | "Load": 0,
12 | "Frequency": 0,
13 | "PowerOutput": 0,
14 | "Voltage": 0,
15 | "Amperes": 0,
16 | "Angle": 0,
17 | "AngularVelocity": 0/60*(2*math.pi),
18 | "Torque": 0,
19 | "Mass": 150000,
20 | "Radius": 2.5,
21 | "Inertia": (1/2) * 7000 * 2.5**2,
22 | "SteamInlet": 0,
23 | }
24 |
25 |
26 |
27 | def run():
28 |
29 | # Calculate the acceleration
30 | Inertia = Turbine["Inertia"]
31 | # Steam Properties
32 | #”Snake… Why are we still here… Just to suffer? Every night, I can feel my leg, and my arm… and even my fingers… The body I’ve lost… and the comrades I’ve lost… It won’t stop hurting, like they’re all still there… You feel it too, don’t you? I’m the one who got caught up with Cipher, a group above nations, even the US, and I was the parasite below, feeding off Zero’s power… They came after you in Cyprus, then Afghanistan. Cipher just… keeps growing, swallowing everything in its path, getting bigger and bigger. Who knows how big now… Boss… I’m gonna make ‘em give back our past… Take back everything that we’ve lost. And I won’t rest… until we do. Our new Mother Base. I don’t know how long it’ll take, but I’ll make it bigger… better than before… Boss...” [The helicopter lands at Mother Base] ”Things have changed, Boss. We pull in money, recruits, just to combat Cipher. Rubbing our noses in bloody battlefield dirt, all for revenge. The world calls for wetwork, and we answer. No greater good, no just cause. Cipher sent us to hell… But we’re going even deeper” [Snake talks] ”I know… I’m already a demon. Heaven’s not my kind of place anyway.” [Kaz resumes] ”Dogs of war, for nine whole years. That ends today. Now you’re not sleeping, and we’re not junkyard hounds... We’re diamond dogs.” [Kaz gets interrupted and laid out on a gurney] [Kaz sits back up] ”We can crush Cipher, Boss. And you can build the army that can do it.” [Snake talks] ”Just one thing, Kaz… This isn’t about the past… We’re fighting for the future.”
33 | if not "flow" in fluid.valves["ms_v_gv1"] or not "flow" in fluid.valves["ms_v_gv2"] or not "flow" in fluid.valves["ms_v_gv3"] or not "flow" in fluid.valves["ms_v_gv4"]:
34 | return
35 | CVCombinedFlow = fluid.valves["ms_v_gv1"]["flow"]+fluid.valves["ms_v_gv2"]["flow"]+fluid.valves["ms_v_gv3"]["flow"]+fluid.valves["ms_v_gv4"]["flow"]
36 | steamInletMass = (CVCombinedFlow*20)
37 | steamInletPressure = pressure.Pressures["Vessel"]
38 |
39 | main_condenser.addHotwellWater(CVCombinedFlow)
40 |
41 | # Electrical Properties
42 | turbineFrequency = (Turbine["AngularVelocity"]/(math.pi))
43 | # Physics
44 |
45 | Torque = 0
46 |
47 | Q = 0
48 | R = 0.287 # kJ/kg-K (specific gas constant for water vapor)
49 | T1 = model.reactor_water_temperature # °C
50 | m_dot = steamInletMass # kg/s
51 | P1 = steamInletPressure
52 | if P1 == 0:
53 | P1 = 1
54 | # Calculate inlet enthalpy
55 | h1 = 2.512 + R*(T1+273) # kJ/kg (from steam tables)
56 | # Estimate isentropic efficiency
57 | eta_s = .9
58 | # Calculate outlet pressure
59 | P2 = P1*(1-eta_s)**(R/0.287) #This is the turbine outlet pressure, might be useful for condenser
60 | #Calculate outlet enthalpy
61 | h2_s = h1 - R*T1*math.log(P2/P1) # isentropic enthalpy drop
62 | h2 = h2_s + R*T1*(1-eta_s) # actual enthalpy drop
63 |
64 | dh = h2-h1
65 | cp = 2
66 | #Calculate turbine torque
67 | Q = m_dot*dh*cp*math.pi**0.5*0.5 # kW (available power)
68 | omega = max(Turbine["AngularVelocity"], 0) # rpm (angular velocity)
69 | T = Q # Nm (torque)
70 |
71 | Torque = T
72 |
73 |
74 | NetTorque = Torque-(((Turbine["AngularVelocity"]+(900/60*(2*math.pi)))**2*60)/5e2)
75 | Turbine["AngularVelocity"] += NetTorque/Inertia
76 | Turbine["AngularVelocity"] = max(Turbine["AngularVelocity"],0)
77 |
78 | if main_generator.Generator["Synchronized"]:
79 | grid_frequency = 60
80 |
81 | Turbine["AngularVelocity"] = grid_frequency*math.pi
82 |
83 | Turbine["Torque"] = Q
84 | Turbine["RPM"] = Turbine["AngularVelocity"]*60/(2*math.pi)
85 | #print(Turbine["RPM"])
86 | Turbine["Frequency"] = turbineFrequency
87 |
88 | model.values["mt_rpm"] = Turbine["RPM"] #TODO: move to DEH
--------------------------------------------------------------------------------
/simulation/models/control_room_columbia/general_physics/pump.py:
--------------------------------------------------------------------------------
1 | from simulation.constants.electrical_types import ElectricalType
2 | from simulation.constants.equipment_states import EquipmentStates
3 | from simulation.models.control_room_columbia.general_physics import ac_power
4 | from simulation.models.control_room_columbia import model
5 | from simulation.models.control_room_columbia.general_physics import fluid
6 | import math
7 | import log
8 |
9 | def clamp(val, clamp_min, clamp_max):
10 | return min(max(val,clamp_min),clamp_max)
11 |
12 | from enum import IntEnum
13 |
14 | class PumpTypes(IntEnum):
15 | Type1 = 0
16 | Type2 = 1
17 |
18 |
19 | pump_1 = { #TODO: improve the accuracy of these calculations
20 | #this pump is motor driven
21 | "motor_breaker_closed" : False,
22 | "was_closed" : False,
23 | "motor_control_switch" : "",
24 | "bus" : "",
25 | "horsepower" : 0,
26 | "watts" : 0,
27 | "amperes" : 0,
28 | "rpm" : 0,
29 | "discharge_press" : 0,
30 | "flow" : 0,
31 | "actual_flow" : 0,
32 | "rated_rpm" : 0,
33 | "rated_discharge_press" : 0,
34 | "npshr" : 1, #feet
35 | "flow_from_rpm" : 0,
36 | "rated_flow" : 0,
37 | "current_limit" : 0,
38 | "header" : "",
39 | "suct_header" : "",
40 | "custom" : False,
41 | "shaft_driven" : False,
42 |
43 | "loop_seq" : False, # Has a LOOP Sequence (otherwise trips instantly)
44 | "loop_avail" : False, # Loop Sequence has permitted loading of this equipment, if needed
45 | "type" : PumpTypes.Type1,
46 | }
47 |
48 | pump_2 = { #TODO: improve the accuracy of these calculations
49 | #this pump is shaft driven (turbine)
50 | "turbine" : "",
51 | "discharge_press" : 0,
52 | "flow" : 0,
53 | "actual_flow" : 0,
54 | "rated_rpm" : 0,
55 | "rated_discharge_press" : 0,
56 | "rated_flow" : 0,
57 | "npshr" : 1,
58 | "header" : "",
59 | "suct_header" : "",
60 | "type" : PumpTypes.Type2,
61 | }
62 |
63 | def initialize_pumps():
64 | for pump_name in model.pumps:
65 | pump = model.pumps[pump_name]
66 | import copy
67 | #TODO: find a better way to do this
68 | if pump["type"] == PumpTypes.Type1:
69 | pump_created = copy.deepcopy(pump_1)
70 | else:
71 | pump_created = copy.deepcopy(pump_2)
72 |
73 | for value_name in pump:
74 | value = pump[value_name]
75 | if value_name in pump_created:
76 | pump_created[value_name] = value
77 |
78 | model.pumps[pump_name] = pump_created
79 |
80 | def calculate_suction(pump,delta):
81 | pump = model.pumps[pump]
82 |
83 | if pump["suct_header"] == "":
84 | return pump["flow"]
85 | #a
86 | if pump["npshr"] < fluid.headers[pump["suct_header"]]["pressure"]*2.3072493927233:
87 | return pump["flow"]
88 | else:
89 | return 0
90 |
91 | def run(delta):
92 | for pump_name in model.pumps:
93 | pump = model.pumps[pump_name]
94 |
95 | if pump["type"] == PumpTypes.Type2:
96 | pump_turbine = model.turbines[pump["turbine"]]
97 | pump["rpm"] = pump_turbine["rpm"]
98 |
99 | pump["flow"] = pump["rated_flow"]*(pump["rpm"]/pump["rated_rpm"])
100 | pump["flow"] = calculate_suction(pump_name,delta)
101 | pump["discharge_pressure"] = pump["rated_discharge_press"]*(pump["rpm"]/pump["rated_rpm"])
102 |
103 | pump["actual_flow"] = fluid.inject_to_header(pump["flow"],pump["discharge_pressure"],fluid.headers[pump["header"]]["temperature"],pump["header"],pump["suct_header"])
104 | continue
105 |
106 | #undervoltage breaker trip
107 |
108 | voltage = 0
109 | try:
110 | pump_bus = ac_power.busses[pump["bus"]]
111 |
112 | voltage = pump_bus.voltage_at_bus()
113 |
114 | if voltage < 120 and pump["motor_breaker_closed"] and not pump["custom"]:
115 | pump["motor_breaker_closed"] = False
116 | pump["was_closed"] = True
117 | continue
118 |
119 | if not pump_name in pump_bus.info["loads"]:
120 | pump_bus.register_load(pump["watts"],pump_name)
121 | else:
122 | pump_bus.modify_load(pump["watts"],pump_name)
123 | except:
124 | #log.warning("Pump does not have an available bus!")
125 | voltage = 4160
126 |
127 | if pump["motor_control_switch"] != "":
128 | if len(model.switches[pump["motor_control_switch"]]["positions"]) > 2:
129 | if model.switches[pump["motor_control_switch"]]["position"] == 2:
130 | pump["motor_breaker_closed"] = True
131 | else:
132 | if model.switches[pump["motor_control_switch"]]["position"] == 1:
133 | pump["motor_breaker_closed"] = True
134 |
135 | if model.switches[pump["motor_control_switch"]]["position"] == 0:
136 | pump["motor_breaker_closed"] = False
137 |
138 | if model.switches[pump["motor_control_switch"]]["lights"] != {}:
139 | model.switches[pump["motor_control_switch"]]["lights"]["green"] = (not pump["motor_breaker_closed"]) and voltage > 120
140 | model.switches[pump["motor_control_switch"]]["lights"]["red"] = pump["motor_breaker_closed"] and voltage > 120
141 |
142 |
143 | if not pump["custom"]:
144 | if pump["loop_avail"] and pump["was_closed"]:
145 | pump["motor_breaker_closed"] = True
146 | pump["was_closed"] = False
147 |
148 | if pump["shaft_driven"]:
149 | pump["flow"] = pump["rated_flow"]*(pump["rpm"]/pump["rated_rpm"])
150 | pump["flow"] = calculate_suction(pump_name,delta)
151 | pump["discharge_pressure"] = pump["rated_discharge_press"]*(pump["rpm"]/pump["rated_rpm"])
152 |
153 | if pump["header"] != "":
154 | pump["actual_flow"] = fluid.inject_to_header(pump["flow"],pump["discharge_pressure"],fluid.headers[pump["header"]]["temperature"],pump["header"],pump["suct_header"])
155 | else:
156 | pump["actual_flow"] = pump["flow"]
157 |
158 | continue
159 |
160 | if pump["motor_breaker_closed"]:
161 | #first, verify that this breaker is allowed to be closed
162 |
163 | #TODO: overcurrent breaker trip
164 |
165 |
166 |
167 | Acceleration = ((pump["rated_rpm"]*(pump_bus.info["frequency"]/60))-pump["rpm"])*1*delta #TODO: Make acceleration and frequency realistic
168 | pump["rpm"] = clamp(pump["rpm"]+Acceleration,0,pump["rated_rpm"]+100)
169 | #full load amperes
170 | if voltage > 0:
171 | AmpsFLA = (pump["horsepower"]*746)/((math.sqrt(3)*voltage*0.876*0.95)) #TODO: variable motor efficiency and power factor
172 | else:
173 | AmpsFLA = 0
174 | pump["amperes"] = (AmpsFLA*clamp(pump["actual_flow"]/pump["rated_flow"],0.48,1))+(AmpsFLA*5*(Acceleration/(pump["rated_rpm"]*1*delta)))
175 | pump["watts"] = voltage*pump["amperes"]*math.sqrt(3)
176 |
177 | #remember to make the loading process for the current (v.FLA*math.clamp(v.flow_with_fullsim etc)) more realistic, and instead make it based on distance from rated rpm (as when the pump is loaded more it will draw more current)
178 | #TODO: better flow calculation
179 | pump["flow"] = pump["rated_flow"]*(pump["rpm"]/pump["rated_rpm"])
180 | pump["flow"] = calculate_suction(pump_name,delta)
181 | pump["discharge_pressure"] = pump["rated_discharge_press"]*(pump["rpm"]/pump["rated_rpm"])
182 |
183 | if pump["header"] != "":
184 | pump["actual_flow"] = fluid.inject_to_header(pump["flow"],pump["discharge_pressure"],fluid.headers[pump["header"]]["temperature"],pump["header"],pump["suct_header"])
185 | else:
186 | pump["actual_flow"] = pump["flow"]
187 | else:
188 | Acceleration = (pump["rpm"])*1*delta #TODO: variable motor accel
189 | pump["rpm"] = clamp(pump["rpm"]-Acceleration,0,pump["rated_rpm"]+100)
190 | pump["amperes"] = 0
191 | pump["watts"] = 0
192 |
193 | pump["flow"] = pump["rated_flow"]*(pump["rpm"]/pump["rated_rpm"])
194 | pump["flow"] = calculate_suction(pump_name,delta)
195 | pump["discharge_pressure"] = pump["rated_discharge_press"]*(pump["rpm"]/pump["rated_rpm"])
196 |
197 | if pump["header"] != "":
198 | pump["actual_flow"] = fluid.inject_to_header(pump["flow"],pump["discharge_pressure"],fluid.headers[pump["header"]]["temperature"],pump["header"],pump["suct_header"])
199 | else:
200 | pump["actual_flow"] = pump["flow"]
201 |
202 |
203 |
204 |
--------------------------------------------------------------------------------
/simulation/models/control_room_columbia/general_physics/tank.py:
--------------------------------------------------------------------------------
1 | import math
2 |
3 | def calculate_level_cylinder(Height,Volume,MassOfLiquid):
4 | #calculate the volume of the cylinder
5 | #piR^2
6 | VolumeOfLiquid = MassOfLiquid*1000000 #to mm^3
7 |
8 | #Because this uses volume, we can simulate water expansion.
9 | return ((VolumeOfLiquid/Volume)*Height) #outputs in mm
10 |
11 | def mm_to_inches(value):
12 | return value/25.4
13 |
14 | def ft_to_mm(value):
15 | return value*304.8
16 |
17 | class TankCylinder:
18 | def __init__(self,name,diameter,height,kg):
19 | """measurements should be in mm"""
20 | self.name = name
21 |
22 | self.diameter = diameter
23 | self.radius = diameter/2
24 | self.height = height
25 |
26 | self.volume = math.pi * (self.radius**2) * self.height #mm^3
27 | self.contents_kg = kg
28 |
29 | self.level = 0
30 |
31 | tanks[self.name] = self
32 |
33 | def get_level(self):
34 |
35 | level = calculate_level_cylinder(self.height,self.volume,self.contents_kg)
36 |
37 | self.level = level
38 | return level
39 |
40 | def add_water(self,amount):
41 | self.contents_kg += amount
42 |
43 | tanks = {}
44 |
45 | #wetwell
46 | TankCylinder("Wetwell",ft_to_mm(87),ft_to_mm(67),3785411.7840007) # 1 million gallons, not sure if its even close to correct
--------------------------------------------------------------------------------
/simulation/models/control_room_columbia/general_physics/turbine.py:
--------------------------------------------------------------------------------
1 | from simulation.models.control_room_columbia import model
2 | from simulation.models.control_room_columbia.reactor_physics import reactor_inventory
3 | from simulation.models.control_room_columbia.general_physics import fluid
4 | from simulation.models.control_room_columbia.general_physics import gas
5 | import math
6 |
7 | def clamp(val, clamp_min, clamp_max):
8 | return min(max(val,clamp_min),clamp_max)
9 |
10 | turbine_template = {
11 | "rpm" : 0,
12 | "rated_rpm" : 6250,
13 | "flow_to_rpm" : 0,
14 | "acceleration_value" : 0,
15 | "trip" : False,
16 | "mechanical_trip" : False,
17 | "trip_valve" : "",
18 | "steam_flow_valve" : "",
19 | "governor_valve" : "",
20 | }
21 |
22 | def initialize_pumps():
23 | for turbine_name in model.turbines:
24 | turbine = model.turbines[turbine_name]
25 | import copy
26 | turbine_created = copy.deepcopy(turbine_template)
27 |
28 | for value_name in turbine:
29 | value = turbine[value_name]
30 | if value_name in turbine_created:
31 | turbine_created[value_name] = value
32 |
33 | model.turbines[turbine_name] = turbine_created
34 |
35 | def run():
36 | for turbine_name in model.turbines:
37 | turbine = model.turbines[turbine_name]
38 |
39 | #TODO: Better simulation
40 | #TODO: Un-hardcode this
41 |
42 | if turbine["trip"] or turbine["mechanical_trip"]:
43 | fluid.valves[turbine["trip_valve"]]["percent_open"] = max(min(fluid.valves[turbine["trip_valve"]]["percent_open"]-10,100),0)
44 |
45 | steam_flow_permitted = fluid.headers["rcic_turbine_steam_line"]
46 |
47 | radius = 100/2
48 | radius = radius*0.1 #to cm
49 |
50 | flow_resistance = (8*50*2000)/(math.pi*(radius**4))
51 |
52 |
53 | flow = (steam_flow_permitted["pressure"]-0)/flow_resistance
54 | flow = abs(flow)
55 |
56 | flow *= (fluid.valves[turbine["trip_valve"]]["percent_open"]/100)
57 | flow *= (fluid.valves[turbine["governor_valve"]]["percent_open"]/100)
58 | #steam_flow_permitted *= (fluid.valves[turbine["steam_flow_valve"]]["percent_open"]/100)
59 | #steam_flow_permitted *= (fluid.valves[turbine["governor_valve"]]["percent_open"]/100)
60 |
61 | acceleration = ((flow/turbine["flow_to_rpm"])-turbine["rpm"])*turbine["acceleration_value"]
62 |
63 | #flow is in cubic centimeters per second
64 | flow = flow/1000 #to liter/s
65 | flow = flow*0.1 #to liter/0.1s (or the sim time)\
66 |
67 | steam_flow_permitted["mass"] -= flow
68 | fluid.headers["rcic_exhaust_steam_line"]["mass"] += flow
69 |
70 | gas.calculate_header_pressure("rcic_turbine_steam_line")
71 | gas.calculate_header_pressure("rcic_exhaust_steam_line")
72 |
73 | turbine["rpm"] += acceleration
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
--------------------------------------------------------------------------------
/simulation/models/control_room_columbia/general_physics/turbine_new.py:
--------------------------------------------------------------------------------
1 | from simulation.models.control_room_columbia import model
2 | from simulation.models.control_room_columbia.reactor_physics import pressure
3 | from simulation.models.control_room_columbia.reactor_physics import steam_functions
4 | from simulation.models.control_room_columbia.general_physics import fluid
5 | from simulation.models.control_room_columbia.general_physics import main_generator
6 | from simulation.models.control_room_columbia.general_physics import main_condenser
7 | import math
8 |
9 | class Turbine:
10 | def __init__(self,name,inlet_header,inlet_nozzle,outlet_header,pump):
11 | self.name = name
12 | self.info = {
13 | "RPM": 0,
14 | "AngularVelocity": 0,
15 | "Torque": 0,
16 | "Mass": 150000,
17 | "Radius": 2.5,
18 | "Inertia": 300 * 6.25,
19 | "InletHeader": inlet_header,
20 | "InletNozzle": inlet_nozzle,
21 | "OutletHeader": outlet_header,
22 | "Pump": pump,
23 | }
24 | turbines[name] = self
25 |
26 | def calculate(self):
27 | # Calculate the acceleration
28 | Inertia = self.info["Inertia"]
29 | # Steam Properties
30 | if not "flow" in fluid.valves[self.info["InletNozzle"]]:
31 | return
32 |
33 | Flow = fluid.valves[self.info["InletNozzle"]]["flow"]
34 | steamInletMass = Flow*150
35 | steamInletPressure = fluid.headers[self.info["InletHeader"]]["pressure"]
36 | #print(steamInletPressure/6895)
37 |
38 | # Physics
39 |
40 | Torque = 0
41 |
42 | Q = 0
43 | R = 0.287 # kJ/kg-K (specific gas constant for water vapor)
44 | T1 = 100 # °C
45 | m_dot = steamInletMass # kg/s
46 | P1 = steamInletPressure
47 | # Calculate inlet enthalpy
48 | h1 = 2.512 + R*(T1+273) # kJ/kg (from steam tables)
49 | # Estimate isentropic efficiency
50 | eta_s = .9
51 | # Calculate outlet pressure
52 | P2 = P1*(1-eta_s)**(R/0.287) #This is the turbine outlet pressure
53 | #Calculate outlet enthalpy
54 | h2_s = h1 - R*T1*math.log(P2/P1) # isentropic enthalpy drop
55 | h2 = h2_s + R*T1*(1-eta_s) # actual enthalpy drop
56 |
57 | dh = h2-h1
58 | cp = 2
59 | #Calculate turbine torque
60 | Q = m_dot*dh*cp*math.pi**0.5*0.5 # kW (available power)
61 | omega = max(self.info["AngularVelocity"], 0) # rpm (angular velocity)
62 | T = Q # Nm (torque)
63 |
64 | Torque = T
65 |
66 |
67 | NetTorque = Torque-(((self.info["AngularVelocity"]+(900/60*(2*math.pi)))**2*60)/5e2)
68 | self.info["AngularVelocity"] += NetTorque/Inertia
69 | self.info["AngularVelocity"] = max(self.info["AngularVelocity"],0)
70 |
71 | self.info["Torque"] = Q
72 | self.info["RPM"] = self.info["AngularVelocity"]*60/(2*math.pi)
73 |
74 | model.pumps[self.info["Pump"]]["rpm"] = self.info["RPM"]
75 |
76 | turbines = {} #TODO: Move to model when done using regular turbine.py
77 |
78 | def initialize():
79 | Turbine("rfw_dt_1a","rft_dt_1a_stop","ms_v_172a","a","rfw_p_1a")
80 | Turbine("rfw_dt_1b","rft_dt_1b_stop","ms_v_172b","a","rfw_p_1b")
81 |
82 | def run():
83 | for turbine in turbines:
84 | turbines[turbine].calculate()
--------------------------------------------------------------------------------
/simulation/models/control_room_columbia/libraries/pid.py:
--------------------------------------------------------------------------------
1 | class PID:
2 | def __init__(self, Kp, Ki, Kd,minimum,maximum):
3 | self.Kp = Kp
4 | self.Ki = Ki
5 | self.Kd = Kd
6 | self.last_error = 0
7 | self.integral = 0
8 | self.minimum = minimum
9 | self.maximum = maximum
10 |
11 | def update(self, setpoint, current, dt):
12 | error = setpoint-current
13 | derivative = (error-self.last_error)/dt
14 | self.integral += error * dt
15 | output = (self.Kp * error) + (self.Ki * self.integral) + (self.Kd * derivative)
16 | self.last_error = error
17 | output = max(min(output,self.maximum),self.minimum) #TODO
18 | return output
19 |
20 | class PIDExperimental:
21 | def __init__(self, Kp, Ki, Kd,minimum,maximum):
22 | self.Kp = Kp
23 | self.Ki = Ki
24 | self.Kd = Kd
25 | self.error = 0
26 | self.proportional = 0
27 | self.derivative = 0
28 | self.last_error = 0
29 | self.integral = 0
30 | self.minimum = minimum
31 | self.maximum = maximum
32 |
33 | def reset(self):
34 | self.error = 0
35 | self.derivative = 0
36 | self.last_error = 0
37 | self.integral = 0
38 |
39 | def update(self, setpoint, current, dt):
40 | error = setpoint-current
41 | derivative = (error -self.last_error)/dt
42 | self.integral += error * dt * self.Ki
43 | output = (self.Kp * error ) + (self.integral) + (self.Kd * derivative)
44 | self.last_error = self.Kd * derivative
45 | self.error = error
46 | self.proportional = self.Kp * error
47 | self.derivative = self.Kd * derivative
48 | output = max(min(output,self.maximum),self.minimum) #TODO
49 | return output
--------------------------------------------------------------------------------
/simulation/models/control_room_columbia/libraries/transient.py:
--------------------------------------------------------------------------------
1 | import matplotlib.pyplot as plt
2 |
3 | class Transient:
4 | def __init__(self,name):
5 | self.graphs = {}
6 | self.shown = False
7 | self.name = name
8 |
9 | def add_graph(self,name):
10 | x = self.graphs.copy()
11 | x[name] = []
12 | y = x
13 | self.graphs = y
14 |
15 | def add(self,name,value):
16 | self.graphs[name].append(value)
17 |
18 | def generate_plot(self):
19 | if self.shown: return
20 | self.shown = True
21 |
22 | fig, ax = plt.subplots()
23 | ax.set_title(self.name)
24 |
25 | for plot_name in self.graphs:
26 | plot = self.graphs[plot_name]
27 |
28 | ax.plot(plot,label=plot_name)
29 |
30 | ax.legend()
31 |
32 |
33 | plt.show()
--------------------------------------------------------------------------------
/simulation/models/control_room_columbia/reactor_physics/cross_sections.py:
--------------------------------------------------------------------------------
1 | import math
2 |
3 | MicroscopicCrossSections = {
4 | "Fuel" : {
5 | "U235" : {
6 | "Thermal" : {
7 | "Scattering" : 10,
8 | "Capture" : 99,
9 | "Fission" : 583,
10 | },
11 | "Fast" : {
12 | "Scattering" : 4,
13 | "Capture" : 0.09,
14 | "Fission" : 1,
15 | }
16 | },
17 | "U238" : {
18 | "Thermal" : {
19 | "Scattering" : 9,
20 | "Capture" : 2,
21 | "Fission" : 0.00002,
22 | },
23 | "Fast" : {
24 | "Scattering" : 5,
25 | "Capture" : 0.07,
26 | "Fission" : 0.3,
27 | }
28 | },
29 | "Pu239" : {
30 | "Thermal" : {
31 | "Scattering" : 8,
32 | "Capture" : 269,
33 | "Fission" : 748,
34 | },
35 | "Fast" : {
36 | "Scattering" : 5,
37 | "Capture" : 0.05,
38 | "Fission" : 2,
39 | }
40 | },
41 | },
42 | "Absorber" : {
43 | "B10" : {
44 | "Thermal" : {
45 | "Scattering" : 2,
46 | "Capture" : 200,
47 | "Fission" : 0,
48 | },
49 | "Fast" : {
50 | "Scattering" : 2,
51 | "Capture" : 0.4,
52 | "Fission" : 0,
53 | }
54 | },
55 | "Cd113" : {
56 | "Thermal" : {
57 | "Scattering" : 100,
58 | "Capture" : 30,
59 | "Fission" : 0,
60 | },
61 | "Fast" : {
62 | "Scattering" : 4,
63 | "Capture" : 0.05,
64 | "Fission" : 0,
65 | }
66 | },
67 | "Xe135" : {
68 | "Thermal" : {
69 | "Scattering" : 400,
70 | "Capture" : 2600000,
71 | "Fission" : 0,
72 | },
73 | "Fast" : {
74 | "Scattering" : 5,
75 | "Capture" : 0.0008,
76 | "Fission" : 0,
77 | }
78 | },
79 | "In115" : {
80 | "Thermal" : {
81 | "Scattering" : 2,
82 | "Capture" : 100,
83 | "Fission" : 0,
84 | },
85 | "Fast" : {
86 | "Scattering" : 4,
87 | "Capture" : 0.02,
88 | "Fission" : 0,
89 | }
90 | },
91 | },
92 | "StructuralMaterials" : {
93 | "Zr90" : {
94 | "Thermal" : {
95 | "Scattering" : 5,
96 | "Capture" : 0.006,
97 | "Fission" : 0,
98 | },
99 | "Fast" : {
100 | "Scattering" : 5,
101 | "Capture" : 0.006,
102 | "Fission" : 0,
103 | }
104 | },
105 | "Fe56" : {
106 | "Thermal" : {
107 | "Scattering" : 10,
108 | "Capture" : 2,
109 | "Fission" : 0,
110 | },
111 | "Fast" : {
112 | "Scattering" : 20,
113 | "Capture" : 0.003,
114 | "Fission" : 0,
115 | }
116 | },
117 | "Cr52" : {
118 | "Thermal" : {
119 | "Scattering" : 3,
120 | "Capture" : 0.5,
121 | "Fission" : 0,
122 | },
123 | "Fast" : {
124 | "Scattering" : 3,
125 | "Capture" : 0.002,
126 | "Fission" : 0,
127 | }
128 | },
129 | "Ni58" : {
130 | "Thermal" : {
131 | "Scattering" : 20,
132 | "Capture" : 3,
133 | "Fission" : 0,
134 | },
135 | "Fast" : {
136 | "Scattering" : 3,
137 | "Capture" : 0.008,
138 | "Fission" : 0,
139 | }
140 | },
141 | "O16" : {
142 | "Thermal" : {
143 | "Scattering" : 4,
144 | "Capture" : 0.0001,
145 | "Fission" : 0,
146 | },
147 | "Fast" : {
148 | "Scattering" : 3,
149 | "Capture" : 0.0000000003,
150 | "Fission" : 0,
151 | }
152 | },
153 | },
154 | "Moderator" : {
155 | "H1" : {
156 | "Thermal" : {
157 | "Scattering" : 20,
158 | "Capture" : 0.2,
159 | "Fission" : 0,
160 | },
161 | "Fast" : {
162 | "Scattering" : 4,
163 | "Capture" : 0.00004,
164 | "Fission" : 0,
165 | }
166 | },
167 | "H2" : {
168 | "Thermal" : {
169 | "Scattering" : 4,
170 | "Capture" : 0.0003,
171 | "Fission" : 0,
172 | },
173 | "Fast" : {
174 | "Scattering" : 3,
175 | "Capture" : 0.000007,
176 | "Fission" : 0,
177 | }
178 | },
179 | "C12" : {
180 | "Thermal" : {
181 | "Scattering" : 5,
182 | "Capture" : 0.002,
183 | "Fission" : 0,
184 | },
185 | "Fast" : {
186 | "Scattering" : 2,
187 | "Capture" : 0.00001,
188 | "Fission" : 0,
189 | }
190 | },
191 | }
192 | }
193 |
194 | MacroscopicCrossSections = {
195 | "Moderator" : {
196 | "H1" : {
197 | "Capture" : MicroscopicCrossSections["Moderator"]["H1"]["Thermal"]["Capture"]
198 | },
199 | "H2" : {
200 | "Capture" : MicroscopicCrossSections["Moderator"]["H2"]["Thermal"]["Capture"]
201 | },
202 | "C12" : {
203 | "Capture" : MicroscopicCrossSections["Moderator"]["C12"]["Thermal"]["Capture"] * 10000000000000000000000.1331046540671051
204 | },
205 | },
206 | "Poisons" : {
207 | "Xe135" : {
208 | "Capture" : MicroscopicCrossSections["Absorber"]["Xe135"]["Thermal"]["Capture"] * 2000000000000000000000.629160592592593
209 | }
210 | },
211 | "Absorbers" : {
212 | "B10" : {
213 | "Capture" : MicroscopicCrossSections["Absorber"]["B10"]["Thermal"]["Capture"] * 10000000000000000000000.38506
214 | }
215 | }
216 | }
217 |
218 |
--------------------------------------------------------------------------------
/simulation/models/control_room_columbia/reactor_physics/fuel.py:
--------------------------------------------------------------------------------
1 | import math
2 |
3 | AtomicNumberDensities = {
4 | "U235" : 2.53*(10**22),
5 | "U238" : 2.51*(10**22),
6 | }
7 |
8 | from simulation.models.control_room_columbia.reactor_physics.cross_sections import MicroscopicCrossSections
9 | from simulation.models.control_room_columbia.reactor_physics.cross_sections import MacroscopicCrossSections
10 |
11 | def clamp(val, min, max):
12 | if val < min: return min
13 | if val > max: return max
14 | return val
15 |
16 |
17 | def get(waterMass, controlDepth, neutronFlux, temperatureFuel, CoreFlow):
18 | #TODO: Core flow (percent of 100)
19 | #TODO: Improve code quality
20 |
21 | N235 = AtomicNumberDensities["U235"]
22 | N238 = AtomicNumberDensities["U238"]
23 |
24 | MacroscopicU235 = 1+(N235*MicroscopicCrossSections["Fuel"]["U235"]["Thermal"]["Capture"])+(N238*MicroscopicCrossSections["Fuel"]["U238"]["Thermal"]["Capture"])
25 | ReproductionFactor = (2.43*MacroscopicU235)/(MacroscopicU235+(MicroscopicCrossSections["Fuel"]["U235"]["Thermal"]["Capture"]*N235*0.435))
26 | U235SumCrossSection = (N235*MicroscopicCrossSections["Fuel"]["U235"]["Thermal"]["Fission"]) + (N235*MicroscopicCrossSections["Fuel"]["U235"]["Thermal"]["Capture"])
27 | CR = controlDepth
28 |
29 | voids = neutronFlux/(2500000000000)
30 |
31 | CoreFlow = CoreFlow
32 |
33 | CoreFlow = clamp(abs((CoreFlow/100)-1),0,1)
34 |
35 | voids = clamp(CoreFlow*voids,0,1)
36 |
37 | voids = clamp(voids,0,0.7)
38 |
39 | CR+=voids
40 | #CR+=(_G.SLCPentaborateGallons/4800)*0.7 #This was a value left over from elsworth, for calculating the loss from SLC (really weirdly, should probably be done with real values)
41 | CR = clamp(CR,0,1)
42 |
43 | U = U235SumCrossSection
44 | M = MacroscopicCrossSections["Moderator"]["C12"]["Capture"]
45 | P = 0
46 | CR = (MacroscopicCrossSections["Absorbers"]["B10"]["Capture"]*(CR*10))
47 |
48 |
49 | ThermalUtilizationFactor = (U)/(U+M+P+CR)
50 |
51 | #TODO: What does any of this mean?
52 | Diameter = 0.375*2.54 # in*2.54 = cm
53 | Radius = 0.1875*2.54 # in*2.54 = cm
54 | Length = 0.625*2.54 # in*2.54 = cm
55 |
56 | def calculateDensityChange(initialDensity, temperature): #thank you goosey for making this part easy to understand
57 | # Constants for UO2
58 | coefficient = 0.000012 # Linear thermal expansion coefficient (/°C)
59 | # Calculate the density change
60 | densityChange = initialDensity * coefficient * temperature
61 | # Calculate the final density
62 | finalDensity = initialDensity - densityChange
63 | # Return the final density
64 | return finalDensity
65 |
66 | UraniumDensity = calculateDensityChange(10.97, temperatureFuel+273.15) # g/cm for UO2
67 | pD = UraniumDensity * Diameter
68 | iEff = 4.45 + 26.6 * math.sqrt(4/pD)
69 |
70 | waterMass = max(230458.9374,waterMass) #Apparently limits to TAF. Do we need this?
71 |
72 | #waterMass = waterMass/473072
73 | waterMass = waterMass/2000
74 |
75 | Nf = 10
76 | Nm = (40**waterMass)
77 | Vf = math.pi*Radius*Length
78 |
79 | Lethargy = 0.3
80 | ScatterCrossSectionModerator = (MicroscopicCrossSections["Moderator"]["H2"]["Thermal"]["Scattering"])/(2.54)
81 |
82 | ResonanceEscapeProbability = math.exp(-((Vf*Nf)/(Lethargy*ScatterCrossSectionModerator*Nm*256.51465299715823))*iEff)
83 |
84 | FastFissionFactor = (1-(1-ResonanceEscapeProbability) * (0.025*2.43*0.93)/(ThermalUtilizationFactor*2.43))
85 |
86 | Width = 3
87 | Length = 5
88 | k = 0.2*(10**1) # width/length obtained from buckling graph for reactors
89 | GeometricBuckling = (k*math.pi/Length)
90 | FastNonLeakageProbability = 1/(1+(0.02*GeometricBuckling**2))
91 |
92 | DiffusionCoefficient = 0.7
93 | SigmaA = 70
94 | Ld = math.sqrt(DiffusionCoefficient/SigmaA)
95 |
96 | ThermalNonLeakageProbability = (1/(1+(Ld**2)+(GeometricBuckling**2)))*2.55
97 |
98 | kEff = ReproductionFactor*ThermalUtilizationFactor*ResonanceEscapeProbability*FastFissionFactor*FastNonLeakageProbability*ThermalNonLeakageProbability
99 |
100 | kStep = (kEff)**0.03
101 |
102 | return {"kStep" : kStep, "MacroU235" : MacroscopicU235, "kEff" : kEff}
103 |
104 |
105 |
--------------------------------------------------------------------------------
/simulation/models/control_room_columbia/reactor_physics/neutrons.py:
--------------------------------------------------------------------------------
1 | import math
2 |
3 | NeutronModeratingParameters = {
4 | "H20" : 0.920,
5 | "D2O" : 0.509,
6 | "C" : 0.152,
7 | }
8 |
9 | def getNeutronVelocity(avg_Neutron_Energy):
10 | return 1.386*(10**6)*math.sqrt(avg_Neutron_Energy) # km/s
11 |
12 | def getNeutronDensity(neutronAmount, volume):
13 | return neutronAmount/volume # cm2
14 |
15 |
16 | def getNeutronFlux(neutronDensity, neutronVelocity):
17 | return neutronDensity * neutronVelocity # n/cm2/s
18 |
19 |
20 | def getNeutronEnergy(temperature):
21 | return 1.386*(10**6)*(temperature+273.15) # MeV
22 |
23 |
24 | def getReactionRate(neutronFlux, macroscopicCrossSectionU235):
25 | return neutronFlux/(2.43*macroscopicCrossSectionU235)/202.5
26 |
27 |
28 | def getThermalPower(reactionRate, Volume):
29 | return reactionRate*Volume*202.5*1.602e-13
30 |
--------------------------------------------------------------------------------
/simulation/models/control_room_columbia/reactor_physics/pressure.py:
--------------------------------------------------------------------------------
1 | import math
2 |
3 | waterMass = 390735 #normal water level
4 | steps = 0
5 |
6 | Pressures = {
7 | "Drywell" : 0,
8 | "Wetwell" : 0,
9 | "Vessel" : 0, #pascals
10 | }
11 | Volumes = {
12 | "Drywell" : (831430.47141),
13 | "Wetwell" : 831430.47141*3,
14 | "Vessel" : 928500.26/2, #liters
15 | }
16 |
17 | def getPressure(steamMass, steamTemperature, volume):
18 |
19 | steamTemperature = 60
20 |
21 | gasConstant = 8.314
22 | molarMassOfSteam = 18.01528
23 | volume = volume/1000 #Liters to m^3
24 | # Variables
25 | massOfSteam = (steamMass)*1000
26 | temperature = steamTemperature+273.15
27 | # Convert mass of steam to moles
28 | molesOfSteam = massOfSteam / molarMassOfSteam
29 | # Calculate the pressure using the ideal gas law
30 | pressure = ((molesOfSteam * gasConstant * temperature) / volume)
31 |
32 | return (pressure)
33 |
34 | GasTypes = {
35 | "Steam" : 18.01528,
36 | "Oxygen" : 88.8102,
37 | "Hydrogen" : 11.1898,
38 | "Nitrogen" : 28.01340
39 | }
40 |
41 | def PartialPressure(GasType:int,Mass:int,Temperature:int,Volume:int):
42 |
43 | MolarMass = GasType
44 | GasConstant = 8.314
45 |
46 | Volume = Volume/1000 #Liters to m^3
47 |
48 | MassGrams = Mass*1000
49 | MassGas = MassGrams/MolarMass
50 | Temperature = Temperature+273.15 #assuming C to K
51 |
52 | Pressure = ((MassGas*GasConstant*Temperature)/Volume)
53 |
54 | return Pressure
--------------------------------------------------------------------------------
/simulation/models/control_room_columbia/reactor_physics/reactor_inventory.py:
--------------------------------------------------------------------------------
1 | import math
2 | import iapws
3 |
4 | # Modules
5 |
6 |
7 |
8 | steps = 0
9 |
10 | Vessel_Height = 21300 #mm
11 | Vessel_Diameter = 7450 #mm
12 | def calculate_level_cylinder(Diameter,Volume):
13 | #calculate the volume of the cylinder
14 | #piR^2
15 | Volume = Volume*1000000
16 |
17 | Radius = Diameter/2
18 | Volume_Of_Vessel = (Radius*Radius)*math.pi
19 | Volume_Of_Vessel = Volume_Of_Vessel*(1/3) #Equipment in the RPV takes up volume
20 | #volume = 2D Area * Height, so
21 | #Height = volume/2D Area
22 | #Because this uses volume, we can simulate water expansion.
23 | return ((Volume/Volume_Of_Vessel)) #we have to convert from mm to inches
24 |
25 | def mm_to_inches(value):
26 | return value/25.4
27 |
28 | rx_level_wr = 35
29 | rx_level_nr = 35
30 | rx_level_fzr = 35
31 |
32 | waterMass = 928500.26*(1/3)#Equipment in the RPV takes up volume
33 | waterMass = waterMass*(2/3) #so we start at a reasonable level
34 | limit_press = False
35 |
36 | def run(delta):
37 |
38 | from simulation.models.control_room_columbia.reactor_physics import reactor_physics
39 | from simulation.models.control_room_columbia.reactor_physics import steam_functions
40 | from simulation.models.control_room_columbia.reactor_physics import pressure
41 | global rx_level_wr
42 | global rx_level_nr
43 | global rx_level_fzr
44 | global waterMass
45 | global limit_press
46 | boilingPoint = steam_functions.getBoilingPointForWater(pressure.Pressures["Vessel"])
47 | vapMass = 0
48 |
49 | from simulation.models.control_room_columbia import model
50 | water_temperature = model.reactor_water_temperature
51 |
52 | if water_temperature > 350:
53 | water_temperature = 350 #The simulation breaks down (errors) after 360C.
54 |
55 | if waterMass <= 1:
56 | waterMass = 1
57 |
58 | if waterMass>0:
59 | vapMass = steam_functions.vaporize(waterMass, water_temperature, pressure.Pressures["Vessel"],delta)
60 | reactor_physics.kgSteam = reactor_physics.kgSteam+max(vapMass["vm"],0)
61 |
62 | waterMass = waterMass - max(vapMass["vm"],0)
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 | NewPress = pressure.getPressure(reactor_physics.kgSteam, water_temperature,pressure.Volumes["Vessel"])
71 | pressure.Pressures["Vessel"] = NewPress
72 |
73 | boilingPoint = steam_functions.getBoilingPointForWater(pressure.Pressures["Vessel"])
74 |
75 | if water_temperature > boilingPoint:
76 | water_temperature = boilingPoint
77 |
78 | if waterMass <= 1:
79 | waterMass = 1
80 |
81 |
82 | model.reactor_water_temperature = water_temperature
83 |
84 |
85 | #Calculation of water volume for shrink/swell from temperature.
86 |
87 | WaterTempK = water_temperature + 273.15
88 |
89 | PressureMPA = pressure.Pressures["Vessel"]/1e6
90 |
91 | WaterInfo = iapws.iapws97._Region1(WaterTempK,PressureMPA)
92 |
93 | WaterVolume = waterMass*WaterInfo["v"]
94 |
95 | WaterVolume *= 1000 #M^3 to mm^3
96 |
97 |
98 | raw_level = mm_to_inches(calculate_level_cylinder(Vessel_Diameter,WaterVolume))
99 | rx_level_wr = raw_level-528.55
100 |
101 | rx_level_fzr = min(-110,rx_level_wr) #find the bottom of this range
102 |
103 | if rx_level_wr > 0:
104 | rx_level_nr = rx_level_wr
105 | else:
106 | rx_level_nr = 0
107 |
108 | def add_water(kg:int):
109 | global waterMass
110 | waterMass+=kg
111 |
112 | def remove_steam(amount):
113 | from simulation.models.control_room_columbia.reactor_physics import reactor_physics
114 | reactor_physics.kgSteam -= amount
--------------------------------------------------------------------------------
/simulation/models/control_room_columbia/reactor_physics/reactor_physics.py:
--------------------------------------------------------------------------------
1 | import math
2 |
3 | # Modules
4 | from simulation.models.control_room_columbia import model
5 | from simulation.models.control_room_columbia.reactor_physics import fuel
6 | from simulation.models.control_room_columbia.reactor_physics import neutrons
7 | from simulation.models.control_room_columbia.reactor_physics import reactor_inventory
8 |
9 |
10 |
11 | kgSteam = 10e2
12 | kgSteamDrywell = 0
13 |
14 | power_before_sd = 0
15 | time_since_sd = 0
16 |
17 | time_run = 0
18 |
19 | def run(delta,rods):
20 | #TODO: Improve code quality, add comments, etc
21 |
22 |
23 | global time_run
24 |
25 | time_run+=delta
26 |
27 | CoreFlow = ((model.pumps["rrc_p_1a"]["flow"] + model.pumps["rrc_p_1b"]["flow"]) / 100000) * 100
28 | waterMass = reactor_inventory.waterMass
29 | old_temp = model.reactor_water_temperature
30 | new_temp = model.reactor_water_temperature
31 |
32 | avg_keff = 0
33 | avg_power = 0
34 |
35 | rod_num = 0
36 |
37 | rods_to_set = {}
38 |
39 | for rod in rods:
40 | rod_num+=1
41 | info = rods[rod]
42 | NeutronFlux = max(info["neutrons"], 100)
43 |
44 | mykEffArgs = fuel.get(waterMass, abs((info["insertion"]/48)-1), NeutronFlux, 60 ,CoreFlow)
45 | mykStep = mykEffArgs["kStep"]
46 | avg_keff += mykEffArgs["kEff"]
47 | if rod in rods_to_set:
48 | rods_to_set[rod] = max((info["neutrons"]+rods_to_set[rod])*mykStep,10)
49 | else:
50 | rods_to_set[rod] = max(info["neutrons"]*mykStep,10)
51 |
52 | info["measured_neutrons"] = info["neutrons"]
53 |
54 | energy = info["neutrons"]/(2500000000000)
55 | avg_power += energy
56 | energy = (energy*3486)#*delta # in mwt
57 |
58 | calories = ((energy*1000000))/185 # divide by number of rods
59 |
60 | HeatC = calories/1000
61 |
62 | TempNow = (HeatC/waterMass)*delta
63 |
64 | new_temp += TempNow
65 |
66 | if info["neutrons"] < 10:
67 | info["neutrons"] = 10
68 |
69 | directions = [
70 | {"x" : 4,"y" : 0},
71 | {"x" : -4,"y" : 0},
72 | {"x" : 0,"y" : 4},
73 | {"x" : 0,"y" : -4}
74 | ]
75 |
76 | neighbors = []
77 |
78 | for direction in directions:
79 |
80 | dirX = direction["x"]
81 | dirY = direction["y"]
82 |
83 | rod_name = "%s-%s" % (str(info["x"]+dirX),str(info["y"]+dirY))
84 | if not rod_name in rods:
85 | nextPosition = {"name": "outside_core","actual_amount":info["neutrons"]*0.94,"count":0}
86 | #neighbors.append(nextPosition)
87 | continue
88 |
89 | if rod_name in rods_to_set:
90 | nextPosition = {"name": rod_name,"actual_amount":rods_to_set[rod_name],"count":rods_to_set[rod_name]}
91 | else:
92 | nextPosition = {"name": rod_name,"actual_amount":rods[rod_name]["neutrons"],"count":0} #set to 0 so the rod can add later
93 |
94 | neighbors.append(nextPosition)
95 |
96 | # simulate transfer
97 |
98 | for neighbor in neighbors:
99 |
100 | def transport_equation():
101 | return (info["neutrons"] - neighbor["actual_amount"])/10*mykStep
102 |
103 | transported = transport_equation()
104 |
105 |
106 |
107 | rods_to_set[neighbor["name"]] = neighbor["count"] + transported
108 |
109 | rods_to_set[rod] = max(rods_to_set[rod] - transported,10)
110 |
111 |
112 | for rod in rods_to_set:
113 | if rod == "outside_core":
114 | continue
115 | info = rods[rod]
116 | if rods_to_set[rod] < 10:
117 | rods_to_set[rod] = 10
118 | info["neutrons"] = rods_to_set[rod]
119 |
120 |
121 | avg_keff = avg_keff/rod_num
122 | avg_power = avg_power/rod_num
123 | avg_power = avg_power
124 |
125 | global time_since_sd
126 | global power_before_sd
127 |
128 |
129 |
130 | if avg_keff < 0.85 or avg_power < 0.02:
131 | #print(avg_keff)
132 | if time_since_sd == 0:
133 | print("!!! Reactor shutdown !!!")
134 | power_before_sd = avg_power
135 |
136 | time_since_sd += 0.1
137 | else:
138 | #reactor no longer shutdown
139 | power_before_sd = 0
140 | time_since_sd = 0
141 |
142 | #Wigner-Way formula for decay heat
143 |
144 | pw = power_before_sd #TODO
145 | t_0 = 100*86400 #100 days to seconds
146 | t = time_since_sd+1
147 |
148 | decay = 0.0622 * pw * ( ( t ** -0.2 ) - ( ( t_0 + t ) ** - 0.2 ) )
149 |
150 | decay *= 1.3 #slight increase for the heat of internals in the core
151 |
152 | heat_generated = decay*3486 #percent core power to mwt
153 |
154 | calories = (heat_generated*1000000)
155 |
156 | HeatC = calories/1000
157 |
158 | TempNow = (HeatC/waterMass)
159 |
160 | new_temp += TempNow
161 |
162 | model.reactor_water_temperature += new_temp-old_temp
163 |
164 | if time_run >= 1:
165 | time_run = 0
166 |
167 |
--------------------------------------------------------------------------------
/simulation/models/control_room_columbia/reactor_physics/steam_functions.py:
--------------------------------------------------------------------------------
1 | import math
2 | import iapws
3 |
4 | def getRegion1Data(temperature,pressure):
5 |
6 | pressure = min(pressure,1.0342e7)
7 |
8 | T = temperature+273.15 #Temperature, [K]
9 | P = pressure/1e6 #Pressure, [MPa]
10 |
11 |
12 | data = iapws.iapws97._Region1(T,P)
13 |
14 |
15 | return data
16 |
17 |
18 | def getBoilingPointForWater(Pressure):
19 | Pressure = Pressure/1e6 #Pressure, [MPa]
20 | try:
21 | boiling_point_k = iapws.iapws97._TSat_P(Pressure)
22 | except:
23 | boiling_point_k = 373.15 #100c
24 |
25 | boiling_point_c = boiling_point_k - 273.15
26 |
27 | return min(max(100,boiling_point_c),350)
28 |
29 |
30 | def vaporize(initialMass, temperature, pressure, delta):
31 | pressure = pressure+1
32 | boilingPoint = getBoilingPointForWater(pressure)
33 |
34 | data = getRegion1Data(temperature,pressure)
35 |
36 | specificHeatWater = data["cp"]
37 | specificVaporEnthalpy = data["h"]
38 |
39 | deltaSteamMass = ((temperature-boilingPoint) * specificHeatWater * initialMass) / specificVaporEnthalpy
40 | vaporizedMass = deltaSteamMass*delta
41 |
42 | return {"vm":vaporizedMass, "bp":boilingPoint}
43 |
44 |
--------------------------------------------------------------------------------
/simulation/models/control_room_columbia/rod_generation.py:
--------------------------------------------------------------------------------
1 | import math
2 | import random
3 |
4 | def generate_lprm(lprm):
5 | from simulation.models.control_room_columbia.neutron_monitoring import local_power_range_monitors
6 |
7 | exempt_lprms = {
8 | "56-49",
9 | "48-57",
10 | }
11 |
12 | if lprm in exempt_lprms: return
13 |
14 | local_power_range_monitors[lprm] = {
15 | "D": {
16 | "power": 0.00,
17 |
18 | "upscale_setpoint": 117.00,
19 | "downscale_setpoint": 25.00,
20 | },
21 | "C": {
22 | "power": 0.00,
23 |
24 | "upscale_setpoint": 117.00,
25 | "downscale_setpoint": 25.00,
26 | },
27 | "B": {
28 | "power": 0.00,
29 |
30 | "upscale_setpoint": 117.00,
31 | "downscale_setpoint": 25.00,
32 | },
33 | "A": {
34 | "power": 0.00,
35 |
36 | "upscale_setpoint": 117.00,
37 | "downscale_setpoint": 25.00,
38 | },
39 | }
40 |
41 | def run(rods,buttons):
42 | x = 18
43 | y = 59
44 | rods_to_generate = 0
45 | rods_generated_row = 0
46 | rods_generated_total = 0
47 |
48 | # our reactor has a total of 185 rods
49 | while rods_generated_total < 185:
50 | # calculate how many control rods we need for each row,
51 | # and our starting position on y (as the rods in a BWR core are in a circular pattern)
52 | if y == 59 or y == 3:
53 | rods_to_generate = 7
54 | x = 18
55 | elif y == 55 or y == 7:
56 | rods_to_generate = 9
57 | x = 14
58 | elif y == 51 or y == 11:
59 | rods_to_generate = 11
60 | x = 10
61 | elif y == 47 or y == 15:
62 | rods_to_generate = 13
63 | x = 6
64 | elif y <= 43 and y >= 19:
65 | rods_to_generate = 15
66 | x = 2
67 |
68 | while x <= 58 and y <= 59:
69 | # create rods
70 | while rods_generated_row < rods_to_generate:
71 | # there's probably a better way to do this...
72 | # i dont know obamacode, is there?
73 | x_str = str(x)
74 | if len(x_str) < 2:
75 | x_str = "0%s" % x_str
76 |
77 | y_str = str(y)
78 | if len(y_str) < 2:
79 | y_str = "0%s" % y_str
80 |
81 | rod_number = "%s-%s" % (x_str, y_str)
82 |
83 | rods[rod_number] = {
84 | "insertion": 0.00,
85 | "scram_pressure": random.randint(50,60),
86 | "scram": False,
87 | "accum_pressure": 1700.00, #normal pressure is around 1700 psig
88 | "accum_trouble": False,
89 | "accum_trouble_acknowledged": False,
90 | "reed_switch_fail" : False,
91 | "drift_alarm": False,
92 | "driftto": -15,
93 | "driving": False,
94 | "select": False,
95 | "atws": False,
96 | "atws_position": 0,
97 | "x" : x,
98 | "y" : y,
99 |
100 | #physics stuff
101 | "neutrons" : 0,
102 | "measured_neutrons" : 0,#250000000000,
103 | "measured_neutrons_last" : 0,#250000000000,
104 | }
105 |
106 | if ((x/4) - 0.5) % 2 != 0: #generate LPRMs
107 | if ((y/4) - 0.75) % 2 != 0:
108 | lprm_x = str(x+2)
109 | if len(lprm_x) < 2:
110 | lprm_x = "0%s" % lprm_x
111 |
112 | lprm_y = str(y+2)
113 | if len(lprm_y) < 2:
114 | lprm_y = "0%s" % lprm_y
115 |
116 | generate_lprm("%s-%s" % (lprm_x, lprm_y))
117 |
118 | # increment y by 4 because we only have a control rod per every four fuel assemblies
119 | x += 4
120 |
121 | # keep track of how many rods we're generating
122 | rods_generated_row += 1
123 | rods_generated_total += 1
124 |
125 | # move on to the next row
126 | rods_generated_row = 0
127 | y -= 4
128 | break
129 |
130 |
--------------------------------------------------------------------------------
/simulation/models/control_room_columbia/systems/cas.py:
--------------------------------------------------------------------------------
1 | #Control And Service air system
2 | from simulation.models.control_room_columbia import model
3 | from simulation.models.control_room_columbia.general_physics import air_system as air
4 | from simulation.models.control_room_columbia.general_physics import ac_power
5 |
6 | STANDBY_AIR_COMP_TIMER = 0 #30 minutes runtime
7 | STANDBY_AIR_COMP_START_PRESS = 100 #psig
8 | STANDBY_AIR_COMP_STOP_PRESS = 105 #psig
9 |
10 | CAS_C_1A = None
11 | CAS_C_1B = None
12 | CAS_C_1C = None
13 |
14 | CAS_AR_1A = None #Air Receiver
15 |
16 | CONTROL_AIR_HEADER = None
17 | SCRAM_AIR_HEADER = None
18 |
19 | CRD_SCRAM_ISOLATE = None #simplified version
20 | CRD_SCRAM_VENT = None #simplified version
21 |
22 | CRD_ATWS_ISOLATE = None #Simplified
23 | CRD_ATWS_VENT = None #Simplified
24 |
25 | SA_PCV_2 = None #SA - CAS Crosstie Isolation (auto)
26 |
27 | VENT = None
28 |
29 |
30 | def init():
31 |
32 | global STANDBY_AIR_COMP_TIMER
33 | global STANDBY_AIR_COMP_START_PRESS
34 | global STANDBY_AIR_COMP_STOP_PRESS
35 |
36 | global CAS_C_1A
37 | global CAS_C_1B
38 | global CAS_C_1C
39 |
40 | global CAS_AR_1A
41 |
42 | global CONTROL_AIR_HEADER
43 |
44 | CONTROL_AIR_HEADER = air.AirHeader(1,120,2)
45 |
46 | CAS_C_1A = air.Compressor(130,air.SupplyElectric(ac_power.busses["7a"],240,None),100)
47 | CAS_C_1B = air.Compressor(130,air.SupplyElectric(ac_power.busses["8a"],240,None),100)
48 | CAS_C_1C = air.Compressor(130,air.SupplyElectric(ac_power.busses["2p"],240,None),100) #MC-2P (off of SL-21 TB MEZZANINE)
49 |
50 | CAS_C_1A.add_loading_system(110,120,CONTROL_AIR_HEADER)
51 | CAS_C_1B.add_loading_system(110,120,CONTROL_AIR_HEADER)
52 | CAS_C_1C.add_loading_system(110,120,CONTROL_AIR_HEADER)
53 |
54 | CAS_AR_1A = air.AirHeader(1,120,15) #there is actually multiple but just one is good enough simulation wise
55 |
56 | CAS_AR_1A.add_feeder(CAS_C_1A)
57 | CAS_AR_1A.add_feeder(CAS_C_1B)
58 | CAS_AR_1A.add_feeder(CAS_C_1C)
59 |
60 | CONTROL_AIR_HEADER.add_feeder(CAS_AR_1A)
61 |
62 | global SCRAM_AIR_HEADER
63 | global CRD_SCRAM_ISOLATE
64 | global CRD_SCRAM_VENT
65 | global CRD_ATWS_ISOLATE
66 | global CRD_ATWS_VENT
67 | global VENT
68 |
69 | SCRAM_AIR_HEADER = air.AirHeader(1,120,0.2) #what is the actual normal pressure?
70 | CRD_SCRAM_ISOLATE = air.Valve(100,None,False,False,500)
71 | CRD_SCRAM_VENT = air.Valve(0,None,False,False,500)
72 |
73 | SCRAM_AIR_HEADER.add_feeder(CONTROL_AIR_HEADER,CRD_SCRAM_ISOLATE)
74 | VENT = air.Vent()
75 | VENT.add_feeder(SCRAM_AIR_HEADER,CRD_SCRAM_VENT)
76 |
77 | def run():
78 | global STANDBY_AIR_COMP_TIMER
79 | CAS_C_1A_STANDBY = False
80 | CAS_C_1B_STANDBY = False
81 | CAS_C_1C_STANDBY = False
82 |
83 | CONTROL_AIR_HEADER.calculate()
84 | CAS_AR_1A.calculate()
85 | SCRAM_AIR_HEADER.calculate()
86 | VENT.calculate()
87 | CAS_C_1A.calculate()
88 | CAS_C_1B.calculate()
89 | CAS_C_1C.calculate()
90 |
91 | model.switches["cas_c_1a"]["lights"]["red"] = CAS_C_1A.motor_breaker_closed
92 | model.switches["cas_c_1a"]["lights"]["green"] = not CAS_C_1A.motor_breaker_closed
93 | model.switches["cas_c_1a"]["lights"]["unloaded"] = CAS_C_1A.unloaded
94 |
95 | model.switches["cas_c_1b"]["lights"]["red"] = CAS_C_1B.motor_breaker_closed
96 | model.switches["cas_c_1b"]["lights"]["green"] = not CAS_C_1B.motor_breaker_closed
97 | model.switches["cas_c_1b"]["lights"]["unloaded"] = CAS_C_1B.unloaded
98 |
99 | model.switches["cas_c_1c"]["lights"]["red"] = CAS_C_1C.motor_breaker_closed
100 | model.switches["cas_c_1c"]["lights"]["green"] = not CAS_C_1C.motor_breaker_closed
101 | model.switches["cas_c_1c"]["lights"]["unloaded"] = CAS_C_1C.unloaded
102 |
103 | match model.switches["cas_c_1a"]["position"]:
104 | case 0:
105 | CAS_C_1A.stop()
106 | case 1:
107 | CAS_C_1A_STANDBY = True
108 | case 2:
109 | CAS_C_1A.start()
110 |
111 | match model.switches["cas_c_1b"]["position"]:
112 | case 0:
113 | CAS_C_1B.stop()
114 | case 1:
115 | CAS_C_1B_STANDBY = True
116 | case 2:
117 | CAS_C_1B.start()
118 |
119 | match model.switches["cas_c_1c"]["position"]:
120 | case 0:
121 | CAS_C_1C.stop()
122 | case 1:
123 | CAS_C_1C_STANDBY = True
124 | case 2:
125 | CAS_C_1C.start()
126 |
127 |
128 | if STANDBY_AIR_COMP_TIMER >= 30*60*10 and STANDBY_AIR_COMP_STOP_PRESS < CONTROL_AIR_HEADER.get_pressure():
129 | model.alarms["standby_air_comp_on"]["alarm"] = False
130 | if STANDBY_AIR_COMP_TIMER > 0:
131 | #Stop standby comps
132 | if CAS_C_1A_STANDBY:
133 | CAS_C_1A.stop()
134 | if CAS_C_1B_STANDBY:
135 | CAS_C_1B.stop()
136 | if CAS_C_1C_STANDBY:
137 | CAS_C_1C.stop()
138 |
139 | STANDBY_AIR_COMP_TIMER = 0
140 |
141 |
142 | if CONTROL_AIR_HEADER.get_pressure() < STANDBY_AIR_COMP_START_PRESS or STANDBY_AIR_COMP_TIMER > 0:
143 | model.alarms["standby_air_comp_on"]["alarm"] = True
144 |
145 | if STANDBY_AIR_COMP_TIMER == 0:
146 | #Start standby comps
147 | if CAS_C_1A_STANDBY:
148 | CAS_C_1A.start()
149 | if CAS_C_1B_STANDBY:
150 | CAS_C_1B.start()
151 | if CAS_C_1C_STANDBY:
152 | CAS_C_1C.start()
153 |
154 | STANDBY_AIR_COMP_TIMER += 1
155 |
156 | CONTROL_AIR_HEADER.fill -= 0.0001 #fixed leak rate
157 |
158 | if CONTROL_AIR_HEADER.get_pressure() < 90: #isolate SA and trigger low press alarm
159 | model.alarms["control_air_hdr_press_low"]["alarm"] = True
160 | else:
161 | model.alarms["control_air_hdr_press_low"]["alarm"] = False
162 |
163 | model.alarms["scram_valve_pilot_air_header_press_low"]["alarm"] = SCRAM_AIR_HEADER.get_pressure() < 60
164 |
165 | model.values["control_air_press"] = CONTROL_AIR_HEADER.get_pressure()
166 |
--------------------------------------------------------------------------------
/simulation/models/control_room_columbia/systems/cia.py:
--------------------------------------------------------------------------------
1 | from simulation.models.control_room_columbia import model
2 | from simulation.models.control_room_columbia.general_physics import air_system as air
3 |
4 | Mainheader = None
5 | MainheaderDrywell = None
6 | ADSAHeader = None
7 | ADSBHeader = None
8 |
9 | CIA_V_39A = None
10 | CIA_V_39B = None
11 | CIA_V_30A = None
12 | CIA_V_30B = None
13 | CIA_V_20 = None
14 |
15 | ISOLTIMER = 0
16 |
17 | def init():
18 | global Mainheader
19 | global MainheaderDrywell
20 | global ADSAHeader
21 | global ADSBHeader
22 |
23 | global CIA_V_39A
24 | global CIA_V_39B
25 | global CIA_V_30A
26 | global CIA_V_30B
27 | global CIA_V_20
28 |
29 | Mainheader = air.AirHeader(1,185)
30 | MainheaderDrywell = air.AirHeader(1,185)
31 | ADSAHeader = air.AirHeader(1,180)
32 | ADSBHeader = air.AirHeader(1,180)
33 |
34 | CIA_V_39A = air.Valve(100,"cia_v_39a",True,True,50,air.SupplyAir(Mainheader,60,air.FailureModes.CLOSED))
35 | CIA_V_39B = air.Valve(100,None,True,True,50,air.SupplyAir(Mainheader,60,air.FailureModes.CLOSED))
36 | CIA_V_30A = air.Valve(100,"cia_v_30a",True,True,15,None) #supply is electric
37 | CIA_V_30B = air.Valve(100,None,True,True,15,None) #supply is electric
38 | CIA_V_20 = air.Valve(100,"cia_v_20",True,True,15,None) #supply is electric
39 |
40 | ADSAHeader.add_feeder(Mainheader,CIA_V_39A)
41 | ADSBHeader.add_feeder(Mainheader,CIA_V_39B)
42 | MainheaderDrywell.add_feeder(Mainheader,CIA_V_20)
43 |
44 | DIV1MANOOS = False
45 | DIV1MANOOSP = False #was pressed, prevents crazy lights
46 |
47 | def run():
48 |
49 | global ISOLTIMER
50 | global DIV1MANOOS
51 | global DIV1MANOOSP
52 |
53 | Mainheader.calculate()
54 | MainheaderDrywell.calculate()
55 | ADSAHeader.calculate()
56 | ADSBHeader.calculate()
57 |
58 | CIA_V_39A.calculate()
59 | CIA_V_39B.calculate()
60 | CIA_V_30A.calculate()
61 | CIA_V_30B.calculate()
62 | CIA_V_20.calculate()
63 |
64 | if Mainheader.get_pressure() < 160: #isolate at 160
65 | if ISOLTIMER < 180: #time delay prevents spurious actuation for temporary high flow rates
66 | ISOLTIMER += 0.1
67 | else:
68 | model.alarms["ads_n2_hdr_a_isolated"]["alarm"] = True
69 | CIA_V_39A.close()
70 | CIA_V_39B.close()
71 | else:
72 | if ISOLTIMER > 0:
73 | model.alarms["ads_n2_hdr_a_isolated"]["alarm"] = False
74 | ISOLTIMER = 0
75 | CIA_V_39A.open()
76 | CIA_V_39B.open()
77 |
78 | DIV1OOS = DIV1MANOOS
79 | BISI1TEST = model.buttons["cia_a_lamp_test"]["state"]
80 |
81 | if model.buttons["cia_a_manual_out_of_serv"]["state"] and not DIV1MANOOSP:
82 | DIV1MANOOSP = True
83 | DIV1MANOOS = not DIV1MANOOS
84 | elif model.buttons["cia_a_manual_out_of_serv"]["state"] == False:
85 | DIV1MANOOSP = False
86 |
87 | model.alarms["cia_a_manual_out_of_serv"]["alarm"] = DIV1MANOOS or BISI1TEST
88 |
89 | if ADSAHeader.get_pressure() < 156:
90 | model.alarms["n2_div_1_supply_press_low"]["alarm"] = True
91 | #TODO: Bottles and programmers
92 | DIV1OOS = True
93 | else:
94 | model.alarms["n2_div_1_supply_press_low"]["alarm"] = BISI1TEST
95 |
96 |
97 |
98 |
99 | model.alarms["cia_div_1_out_of_serv"]["alarm"] = DIV1OOS
100 |
101 | model.values["cia_main_header_press"] = Mainheader.get_pressure()
102 | model.values["cia_ads_a_header_press"] = ADSAHeader.get_pressure()
103 |
104 | #TODO: Div 2 BISI
105 |
106 |
107 |
108 |
109 |
110 |
--------------------------------------------------------------------------------
/simulation/models/control_room_columbia/systems/circ_water.py:
--------------------------------------------------------------------------------
1 | from simulation.constants import pump
2 | from simulation.models.control_room_columbia.general_physics import air_system as air #TODO: use a generalized version for valves instead
3 |
4 | CW_P_1A = None
5 | CW_V_1A = None #P1A discharge butterfly
6 |
7 | def init():
8 |
9 | global CW_P_1A
10 | global CW_V_1A
11 |
12 | CW_P_1A = pump.MotorPump("cw_p_1a",bus="1",horsepower=5060,rated_rpm=1800,rated_discharge_press=40,rated_flow=186000)
13 | CW_V_1A = air.Valve(0,"cw_v_1a",True,False,60,air.SupplyElectric())
14 |
15 | print("Circ water init")
16 |
17 | def run(delta):
18 |
19 | CW_P_1A.calculate(delta)
20 | CW_V_1A.calculate()
21 |
22 | #B and C CW pump trip at level two (https://adamswebsearch2.nrc.gov/webSearch2/main.jsp?AccessionNumber=ML070740688)
23 |
24 | #switch START position opens the discharge valve
25 | #switch STOP position strokes closed the discharge valve, stops pump at 15% open and continues closing after
26 | #switch PTL position strokes closed the discharge valve asap
27 |
28 |
--------------------------------------------------------------------------------
/simulation/models/control_room_columbia/systems/condensate.py:
--------------------------------------------------------------------------------
1 | from simulation.models.control_room_columbia import model
2 | from simulation.models.control_room_columbia.general_physics import fluid
3 |
4 | def run():
5 | model.values["cond_discharge_press"] = fluid.headers["condensate_discharge"]["pressure"]/6895
6 | model.values["cond_p_1a_amps"] = model.pumps["cond_p_1a"]["amperes"]
7 | model.values["cond_p_1b_amps"] = model.pumps["cond_p_1b"]["amperes"]
8 | model.values["cond_p_1c_amps"] = model.pumps["cond_p_1c"]["amperes"]
9 |
10 | model.values["cond_booster_discharge_press"] = fluid.headers["condensate_booster_discharge"]["pressure"]/6895
11 | model.values["cond_p_2a_amps"] = model.pumps["cond_p_2a"]["amperes"]
12 | model.values["cond_p_2b_amps"] = model.pumps["cond_p_2b"]["amperes"]
13 | model.values["cond_p_2c_amps"] = model.pumps["cond_p_2c"]["amperes"]
--------------------------------------------------------------------------------
/simulation/models/control_room_columbia/systems/control_rod_drive.py:
--------------------------------------------------------------------------------
1 | from simulation.models.control_room_columbia import model
2 | from simulation.models.control_room_columbia.reactor_physics import pressure
3 | from simulation.models.control_room_columbia.general_physics import fluid
4 | from simulation.models.control_room_columbia.libraries.pid import PID
5 |
6 | crd_fic_600 = None
7 | valve_open = 0
8 |
9 | def initialize():
10 | global crd_fic_600
11 | crd_fic_600 = PID(Kp=0.1, Ki=0.00000001, Kd=0.15, minimum=-10,maximum=10)
12 |
13 | def run():
14 | global valve_open
15 | model.values["crd_p_1a_amps"] = model.pumps["crd_p_1a"]["amperes"]
16 | model.values["crd_p_1b_amps"] = model.pumps["crd_p_1b"]["amperes"]
17 |
18 | model.values["crd_system_flow"] = model.pumps["crd_p_1a"]["actual_flow"] + model.pumps["crd_p_1b"]["actual_flow"]
19 | model.values["charge_header_pressure"] = fluid.headers["crd_discharge"]["pressure"]/6895
20 | model.values["drive_header_flow"] = 0 #TODO
21 | model.values["cooling_header_flow"] = fluid.valves["cooling_to_reactor"]["flow"]*15.85032 #liter/s to gpm
22 |
23 | model.values["drive_header_dp"] = (fluid.headers["drive_water_header"]["pressure"]/6895) - (pressure.Pressures["Vessel"]/6895)
24 | model.values["cooling_header_dp"] = (fluid.headers["cooling_water_header"]["pressure"]/6895) - (pressure.Pressures["Vessel"]/6895)
25 |
26 | speed_control_signal = crd_fic_600.update(50,model.pumps["crd_p_1a"]["actual_flow"] + model.pumps["crd_p_1b"]["actual_flow"],1) #TODO: Allow changing of in-use valve
27 |
28 | valve_open = max(min(valve_open+speed_control_signal,100),0)
29 |
30 | fluid.valves["crd_fcv_2a"]["percent_open"] = valve_open
31 |
32 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/simulation/models/control_room_columbia/systems/deh.py:
--------------------------------------------------------------------------------
1 | from simulation.models.control_room_columbia import model
2 | from simulation.models.control_room_columbia import reactor_protection_system
3 | from simulation.models.control_room_columbia.general_physics import fluid
4 | from simulation.models.control_room_columbia.general_physics import main_turbine
5 | from simulation.models.control_room_columbia.general_physics import main_generator
6 | from simulation.models.control_room_columbia.general_physics import main_condenser
7 | from simulation.models.control_room_columbia.reactor_physics import pressure
8 | from simulation.models.control_room_columbia.libraries.pid import PID
9 | import math
10 |
11 | setpoint = 950 #pressure drop is ~ 50 psig across the main steam system
12 |
13 | #Despite this being named DEH, we are using an EHC. Rename this? TODO
14 |
15 | PressureController = None
16 | SpeedController = None
17 | gov_valve = 0
18 | bypass_valve = 0
19 | last_speed = 0
20 |
21 | turbine_trip = False
22 | mechanical_lockout = False
23 |
24 | SpeedReference = {
25 | "ehc_closed" : -500,
26 | "ehc_100" : 100,
27 | "ehc_500" : 500,
28 | "ehc_1500" : 1500,
29 | "ehc_1800" : 1800,
30 | "ehc_overspeed" : 9999,
31 | }
32 |
33 | AccelerationReference = {
34 | "ehc_slow" : 10,
35 | "ehc_med" : 40,
36 | "ehc_fast" : 70,
37 | }
38 |
39 | line_speed = {
40 | "on" : False, #TODO: Figure out how exactly the Line Speed Matcher works (works with the Desired Load Control?)
41 | }
42 |
43 | LoadSetpoint = 1100
44 |
45 | SelectedAccelerationReference = 10
46 |
47 | SelectedSpeedReference = -500
48 |
49 | mech_trip_lockout = False #Locks out the mechanical trip device to make sure it doesnt initiate an actual turbine trip
50 | mech_trip_test = False #Tests the mechanical trip device. If the system isnt locked out before, this will initiate a turbine trip
51 |
52 | def initialize():
53 | #initialize our PIDs:
54 | global PressureController
55 | PressureController = PID(Kp=0.1, Ki=0.00000001, Kd=0.15, minimum=-0.3,maximum=0.3)
56 |
57 | global SpeedController
58 | SpeedController = PID(Kp=0.2, Ki=0.00000001, Kd=0.13, minimum=-0.04,maximum=0.03)
59 |
60 | global AccelerationController
61 | AccelerationController = PID(Kp=0.02, Ki=0.000001, Kd=0.22, minimum=-0.03,maximum=0.03)
62 |
63 | global LoadController
64 | LoadController = PID(Kp=0.2, Ki=0.00000001, Kd=0.13, minimum=0,maximum=0.04)
65 | #DT is DeltaTime (just use 1 for now)
66 |
67 | def run():
68 |
69 | #Speed Control
70 | global setpoint
71 | global SelectedSpeedReference
72 | global SelectedAccelerationReference
73 | global LoadSetpoint
74 |
75 | global gov_valve
76 | global bypass_valve
77 |
78 | global last_speed
79 | global turbine_trip
80 |
81 | #Main turbine trips
82 |
83 | #get the Turbine Speed
84 | rpm = main_turbine.Turbine["RPM"]
85 | reset_permissive = True
86 |
87 | if rpm > 1980: #Mechanical Overspeed 110% normal (First trip)
88 | turbine_trip = True
89 | reset_permissive = False
90 | model.indicators["mech_trip_tripped"] = True
91 | model.indicators["mech_trip_resetting"] = False
92 | model.indicators["mech_trip_reset"] = False
93 | elif rpm < 1980 and rpm > 1800 and model.indicators["mech_trip_tripped"] == True:
94 | model.indicators["mech_trip_resetting"] = True
95 | reset_permissive = False
96 | else:
97 | model.indicators["mech_trip_tripped"] = False
98 | model.indicators["mech_trip_resetting"] = False
99 | model.indicators["mech_trip_reset"] = True
100 |
101 | global mech_trip_test
102 | global mech_trip_lockout
103 |
104 | if model.buttons["mech_trip_normal"]["state"]:
105 | mech_trip_lockout = False
106 |
107 | if model.buttons["mech_trip_lockout"]["state"]:
108 | mech_trip_lockout = True
109 |
110 | if model.buttons["mech_trip_oiltrip"]["state"]:
111 | mech_trip_test = True
112 |
113 | if model.buttons["mech_trip_reset_pb"]["state"]:
114 | mech_trip_test = True
115 |
116 | #TODO: Trigger annunciator overspeed lockout on mech_trip_lockout
117 |
118 | if rpm > 2016: #Electrical Overspeed 112% normal (Aux trip)
119 | turbine_trip = True
120 | reset_permissive = False
121 |
122 | if main_condenser.MainCondenserBackPressure < 22.5: #22.5 in.hg Loss of heat sink
123 | turbine_trip = True
124 | reset_permissive = False
125 | model.indicators["vacuum_low"] = True
126 | model.indicators["vacuum_tripped"] = True
127 | model.indicators["vacuum_normal"] = False
128 | model.indicators["vacuum_reset"] = False
129 | else:
130 | model.indicators["vacuum_low"] = False
131 | model.indicators["vacuum_normal"] = True
132 |
133 | if model.buttons["mt_trip_1"]["state"]:
134 | turbine_trip = True
135 |
136 | if model.buttons["mt_reset_pb"]["state"] and reset_permissive:
137 | turbine_trip = False
138 | model.indicators["vacuum_tripped"] = False
139 | model.indicators["vacuum_reset"] = True
140 |
141 | if turbine_trip:
142 | model.indicators["mt_tripped"] = True
143 | model.indicators["mt_reset"] = False
144 | fluid.valves["ms_v_sv1"]["percent_open"] = min(max(fluid.valves["ms_v_sv1"]["percent_open"]-25,0),100)
145 | fluid.valves["ms_v_sv2"]["percent_open"] = min(max(fluid.valves["ms_v_sv2"]["percent_open"]-25,0),100)
146 | fluid.valves["ms_v_sv3"]["percent_open"] = min(max(fluid.valves["ms_v_sv3"]["percent_open"]-25,0),100)
147 | fluid.valves["ms_v_sv4"]["percent_open"] = min(max(fluid.valves["ms_v_sv4"]["percent_open"]-25,0),100)
148 | else:
149 | model.indicators["mt_tripped"] = False
150 | model.indicators["mt_reset"] = True
151 | fluid.valves["ms_v_sv1"]["percent_open"] = min(max(fluid.valves["ms_v_sv1"]["percent_open"]+25,0),100)
152 | fluid.valves["ms_v_sv2"]["percent_open"] = min(max(fluid.valves["ms_v_sv2"]["percent_open"]+25,0),100)
153 | fluid.valves["ms_v_sv3"]["percent_open"] = min(max(fluid.valves["ms_v_sv3"]["percent_open"]+25,0),100)
154 | fluid.valves["ms_v_sv4"]["percent_open"] = min(max(fluid.valves["ms_v_sv4"]["percent_open"]+25,0),100)
155 |
156 | for button in SpeedReference:
157 | if model.buttons[button]["state"]:
158 | SelectedSpeedReference = SpeedReference[button]
159 | model.indicators[button] = True
160 | #set each other one to off
161 | for b in SpeedReference:
162 | if b != button:
163 | model.indicators[b] = False
164 |
165 | if model.buttons["ehc_line_speed_off"]["state"]:
166 | line_speed["on"] = False
167 |
168 | if model.buttons["ehc_line_speed_selected"]["state"]:
169 | line_speed["on"] = True
170 |
171 | target_rpm = SelectedSpeedReference
172 |
173 | if line_speed["on"]:
174 | target_rpm = 60.05*math.pi
175 | target_rpm = target_rpm*(30/math.pi)
176 |
177 | model.indicators["ehc_line_speed_operating"] = True
178 | model.indicators["ehc_line_speed_off"] = False
179 | else:
180 | model.indicators["ehc_line_speed_operating"] = False
181 | model.indicators["ehc_line_speed_selected"] = False
182 | model.indicators["ehc_line_speed_off"] = True
183 |
184 | speed_control_signal = SpeedController.update(target_rpm,rpm,1)
185 |
186 | gov_valve = max(min(gov_valve+speed_control_signal,100),0)
187 |
188 | Acceleration = max((rpm-last_speed)*100,0)
189 |
190 | for button in AccelerationReference:
191 | if model.buttons[button]["state"]:
192 | SelectedAccelerationReference = AccelerationReference[button]
193 | model.indicators[button] = True
194 | #set each other one to off
195 | for b in AccelerationReference:
196 | if b != button:
197 | model.indicators[b] = False
198 | if SelectedSpeedReference == -500:
199 | gov_valve = max(min(gov_valve-0.5,100),0)
200 |
201 | if main_generator.Generator["Synchronized"]:
202 | Load = main_generator.Generator["Output"]/1e6
203 | load_control_signal = LoadController.update(LoadSetpoint,Load,1)
204 |
205 | pressure_control_signal = PressureController.update(setpoint,pressure.Pressures["Vessel"]/6895,1)
206 |
207 | gov_valve = max(min(gov_valve+(load_control_signal-pressure_control_signal)-0.01,100),0)
208 |
209 | bypass_valve = max(bypass_valve - 0.2,0)
210 |
211 | #print(Load)
212 | else:
213 | acceleration_control_signal = AccelerationController.update(SelectedAccelerationReference,Acceleration,1)
214 |
215 | gov_valve = max(min(gov_valve+acceleration_control_signal,100),0)
216 |
217 | pressure_control_signal = PressureController.update(setpoint,pressure.Pressures["Vessel"]/6895,0.1)
218 |
219 | bypass_valve = max(min(bypass_valve-pressure_control_signal,100),0)
220 |
221 |
222 | fluid.valves["ms_v_gv1"]["percent_open"] = gov_valve
223 | fluid.valves["ms_v_gv2"]["percent_open"] = gov_valve
224 | fluid.valves["ms_v_gv3"]["percent_open"] = gov_valve
225 | fluid.valves["ms_v_gv4"]["percent_open"] = gov_valve
226 |
227 | fluid.valves["ms_v_160a"]["percent_open"] = bypass_valve
228 | fluid.valves["ms_v_160b"]["percent_open"] = bypass_valve
229 | fluid.valves["ms_v_160c"]["percent_open"] = bypass_valve
230 | fluid.valves["ms_v_160d"]["percent_open"] = bypass_valve
231 |
232 | last_speed = rpm
233 |
234 |
235 |
236 |
237 |
238 |
239 |
240 |
241 |
242 |
--------------------------------------------------------------------------------
/simulation/models/control_room_columbia/systems/diesels.py:
--------------------------------------------------------------------------------
1 |
2 | from simulation.models.control_room_columbia import model
3 | from simulation.models.control_room_columbia.general_physics import ac_power
4 | from simulation.models.control_room_columbia.general_physics import diesel_generator
5 |
6 |
7 |
8 | def run():
9 |
10 | if model.switches["cb_dg1_7_mode"]["position"] == 0: #Switch in C.R.
11 | if model.switches["cb_dg1_7"]["position"] == 0:
12 | ac_power.breakers["cb_dg1_7"].open()
13 | elif model.switches["cb_dg1_7"]["position"] == 2 and ac_power.breakers["cb_dg1_7"].info["sync"]:
14 | ac_power.breakers["cb_dg1_7"].close()
15 |
16 | if model.switches["cb_dg2_8_mode"]["position"] == 0: #Switch in C.R.
17 | if model.switches["cb_dg2_8"]["position"] == 0:
18 | ac_power.breakers["cb_dg2_8"].open()
19 | elif model.switches["cb_dg2_8"]["position"] == 2 and ac_power.breakers["cb_dg2_8"].info["sync"]:
20 | ac_power.breakers["cb_dg2_8"].close()
21 |
22 | model.switches["cb_dg1_7"]["lights"]["green"] = not ac_power.breakers["cb_dg1_7"].info["closed"]
23 | model.switches["cb_dg1_7"]["lights"]["red"] = ac_power.breakers["cb_dg1_7"].info["closed"]
24 |
25 | model.switches["cb_dg2_8"]["lights"]["green"] = not ac_power.breakers["cb_dg2_8"].info["closed"]
26 | model.switches["cb_dg2_8"]["lights"]["red"] = ac_power.breakers["cb_dg2_8"].info["closed"]
27 |
28 | if model.switches["dg1_gov"]["position"] == 0:
29 | diesel_generator.dg1.dg["rpm_set"] -= 0.1
30 | elif model.switches["dg1_gov"]["position"] == 2:
31 | diesel_generator.dg1.dg["rpm_set"] += 0.1
32 |
33 | if model.switches["dg2_gov"]["position"] == 0:
34 | diesel_generator.dg2.dg["rpm_set"] -= 0.1
35 | elif model.switches["dg2_gov"]["position"] == 2:
36 | diesel_generator.dg2.dg["rpm_set"] += 0.1
--------------------------------------------------------------------------------
/simulation/models/control_room_columbia/systems/ehc.py:
--------------------------------------------------------------------------------
1 | from simulation.models.control_room_columbia import model
2 | from simulation.models.control_room_columbia.general_physics import fluid
3 | from simulation.models.control_room_columbia.general_physics import main_turbine
4 | from simulation.models.control_room_columbia.general_physics import main_generator
5 | from simulation.models.control_room_columbia.general_physics import main_condenser
6 | from simulation.models.control_room_columbia.reactor_physics import pressure
7 | from simulation.models.control_room_columbia.libraries.pid import PID
8 | import math
9 |
10 | setpoint = 920
11 |
12 | SpeedController = None
13 | last_speed = 0
14 |
15 | turbine_trip = False
16 | mechanical_lockout = False
17 |
18 | SpeedReference = {
19 | "ehc_closed" : -500,
20 | "ehc_100" : 100,
21 | "ehc_500" : 500,
22 | "ehc_1500" : 1500,
23 | "ehc_1800" : 1800,
24 | "ehc_overspeed" : 9999,
25 | }
26 |
27 | AccelerationReference = {
28 | "ehc_slow" : 10,
29 | "ehc_med" : 40,
30 | "ehc_fast" : 70,
31 | }
32 |
33 | LoadSetpoint = 50
34 | SelectedSpeedReference = -500
35 | SelectedAccelerationReference = 10
36 | LoadLimitSetpoint = 100
37 |
38 | def initialize():
39 | #initialize our PIDs:
40 |
41 | global SpeedController
42 | SpeedController = PID(Kp=0.01, Ki=0, Kd=0.13, minimum=-10,maximum=100)
43 |
44 | global AccelerationController
45 | AccelerationController = PID(Kp=0.005, Ki=0.000001, Kd=0.22, minimum=0,maximum=10)
46 |
47 | global LoadController
48 | LoadController = PID(Kp=0.2, Ki=0.00000001, Kd=0.13, minimum=0,maximum=100)
49 |
50 | def turbine_trips():
51 |
52 | global turbine_trip
53 |
54 | rpm = main_turbine.Turbine["RPM"]
55 | reset_permissive = True
56 |
57 | if rpm > 1980: #Mechanical Overspeed 110% normal (First trip)
58 | turbine_trip = True
59 | reset_permissive = False
60 | model.indicators["mech_trip_tripped"] = True
61 | model.indicators["mech_trip_resetting"] = False
62 | model.indicators["mech_trip_reset"] = False
63 | elif rpm < 1980 and rpm > 1800 and model.indicators["mech_trip_tripped"] == True:
64 | model.indicators["mech_trip_resetting"] = True
65 | reset_permissive = False
66 | else:
67 | model.indicators["mech_trip_tripped"] = False
68 | model.indicators["mech_trip_resetting"] = False
69 | model.indicators["mech_trip_reset"] = True
70 |
71 | if rpm > 2016: #Electrical Overspeed 112% normal (Aux trip)
72 | turbine_trip = True
73 | reset_permissive = False
74 |
75 | if main_condenser.MainCondenserBackPressure < 22.5: #22.5 in.hg Loss of heat sink
76 | turbine_trip = True
77 | reset_permissive = False
78 | model.indicators["vacuum_low"] = True
79 | model.indicators["vacuum_tripped"] = True
80 | model.indicators["vacuum_normal"] = False
81 | model.indicators["vacuum_reset"] = False
82 | else:
83 | model.indicators["vacuum_low"] = False
84 | model.indicators["vacuum_normal"] = True
85 |
86 | if model.buttons["mt_trip_1"]["state"]:
87 | turbine_trip = True
88 |
89 | if model.buttons["mt_reset_pb"]["state"] and reset_permissive:
90 | turbine_trip = False
91 | model.indicators["vacuum_tripped"] = False
92 | model.indicators["vacuum_reset"] = True
93 |
94 | if turbine_trip:
95 | model.indicators["mt_tripped"] = True
96 | model.indicators["mt_reset"] = False
97 | fluid.valves["ms_v_sv1"]["percent_open"] = min(max(fluid.valves["ms_v_sv1"]["percent_open"]-25,0),100)
98 | fluid.valves["ms_v_sv2"]["percent_open"] = min(max(fluid.valves["ms_v_sv2"]["percent_open"]-25,0),100)
99 | fluid.valves["ms_v_sv3"]["percent_open"] = min(max(fluid.valves["ms_v_sv3"]["percent_open"]-25,0),100)
100 | fluid.valves["ms_v_sv4"]["percent_open"] = min(max(fluid.valves["ms_v_sv4"]["percent_open"]-25,0),100)
101 | else:
102 | model.indicators["mt_tripped"] = False
103 | model.indicators["mt_reset"] = True
104 | fluid.valves["ms_v_sv1"]["percent_open"] = min(max(fluid.valves["ms_v_sv1"]["percent_open"]+25,0),100)
105 | fluid.valves["ms_v_sv2"]["percent_open"] = min(max(fluid.valves["ms_v_sv2"]["percent_open"]+25,0),100)
106 | fluid.valves["ms_v_sv3"]["percent_open"] = min(max(fluid.valves["ms_v_sv3"]["percent_open"]+25,0),100)
107 | fluid.valves["ms_v_sv4"]["percent_open"] = min(max(fluid.valves["ms_v_sv4"]["percent_open"]+25,0),100)
108 |
109 | def SpeedControlUnit(rpm:int):
110 |
111 | global last_speed
112 | global SelectedSpeedReference
113 | global SelectedAccelerationReference
114 |
115 | for button in SpeedReference:
116 | if turbine_trip and SpeedReference[button] == -500:
117 | #select CV closed
118 | model.indicators[button] = True
119 | SelectedSpeedReference = -500
120 | continue
121 | elif turbine_trip:
122 | model.indicators[button] = False
123 | continue
124 |
125 | if model.buttons[button]["state"]:
126 | SelectedSpeedReference = SpeedReference[button]
127 | model.indicators[button] = True
128 | #set each other one to off
129 | for b in SpeedReference:
130 | if b != button:
131 | model.indicators[b] = False
132 |
133 | speed_control_signal = SpeedController.update(SelectedSpeedReference,rpm,1)
134 |
135 | Acceleration = max((rpm-last_speed)*100,0)
136 |
137 | for button in AccelerationReference:
138 | if model.buttons[button]["state"]:
139 | SelectedAccelerationReference = AccelerationReference[button]
140 | model.indicators[button] = True
141 | #set each other one to off
142 | for b in AccelerationReference:
143 | if b != button:
144 | model.indicators[b] = False
145 |
146 | acceleration_control_signal = AccelerationController.update(SelectedAccelerationReference,Acceleration,1)
147 | last_speed = rpm
148 |
149 | return max(min(speed_control_signal,100),0)
150 |
151 | def PressureControlUnit(ThrottlePress:int):
152 |
153 | Diff = ThrottlePress-setpoint
154 |
155 | Demand = (Diff/30)*100
156 |
157 | return max(min(Demand,100),0)
158 | #TODO: A/B Unit
159 |
160 | def LoadControlUnit(Load:int):
161 |
162 | Demand = LoadController.update(LoadSetpoint,Load,1)
163 |
164 | global LoadLimitSetpoint
165 |
166 | if model.switches["load_limit"]["position"] == 2:
167 | LoadLimitSetpoint = min(LoadLimitSetpoint-0.1,100)
168 | elif model.switches["load_limit"]["position"] == 0:
169 | LoadLimitSetpoint = min(LoadLimitSetpoint+0.1,100)
170 |
171 | model.values["mt_load"] = Load
172 | model.values["mt_load_set"] = LoadSetpoint
173 |
174 | return Demand
175 |
176 | def run():
177 |
178 | turbine_trips()
179 |
180 | #Speed Control Unit
181 | Speed_Control = SpeedControlUnit(main_turbine.Turbine["RPM"])
182 |
183 | #Pressure Control Unit
184 | Pressure_Control = PressureControlUnit(pressure.Pressures["Vessel"]/6895)#fluid.headers["main_steam_line_d_tunnel"]["pressure"]/6895)
185 |
186 | #Load Control Unit
187 | Load_Control = LoadControlUnit(main_generator.Generator["Output"]/1e6)
188 |
189 | #Load Limiter
190 |
191 | LoadLimitTest = min(Speed_Control,Load_Control,Pressure_Control)
192 |
193 | model.indicators["load_limit_limiting"] = bool(LoadLimitTest >= LoadLimitSetpoint)
194 |
195 | Output = min(Speed_Control,Load_Control,Pressure_Control,LoadLimitSetpoint)
196 |
197 | fluid.valves["ms_v_gv1"]["percent_open"] = Output
198 | fluid.valves["ms_v_gv2"]["percent_open"] = Output
199 | fluid.valves["ms_v_gv3"]["percent_open"] = Output
200 | fluid.valves["ms_v_gv4"]["percent_open"] = Output
201 |
202 | OutputBypass = max(Pressure_Control-Output,0)
203 |
204 | fluid.valves["ms_v_160a"]["percent_open"] = OutputBypass
205 | fluid.valves["ms_v_160b"]["percent_open"] = OutputBypass
206 | fluid.valves["ms_v_160c"]["percent_open"] = OutputBypass
207 | fluid.valves["ms_v_160d"]["percent_open"] = OutputBypass
208 |
209 | model.values["bypass_valve1"] = OutputBypass
210 | model.values["bypass_valve2"] = OutputBypass
211 | model.values["bypass_valve3"] = OutputBypass
212 | model.values["bypass_valve4"] = OutputBypass
--------------------------------------------------------------------------------
/simulation/models/control_room_columbia/systems/feedwater.py:
--------------------------------------------------------------------------------
1 | from simulation.models.control_room_columbia import model
2 | from simulation.models.control_room_columbia import reactor_protection_system
3 | from simulation.models.control_room_columbia import neutron_monitoring
4 | from simulation.models.control_room_columbia.reactor_physics import reactor_inventory
5 | from simulation.models.control_room_columbia.reactor_physics import pressure
6 | from simulation.models.control_room_columbia.general_physics import fluid
7 | #from simulation.models.control_room_columbia.libraries import transient
8 |
9 | requested_setpoint = 35
10 | actual_setpoint = 35
11 |
12 |
13 | #TODO: move this somewhere better
14 | class PID:
15 | def __init__(self, Kp, Ki, Kd):
16 | self.Kp = Kp
17 | self.Ki = Ki
18 | self.Kd = Kd
19 | self.last_error = 0
20 | self.integral = 0
21 |
22 | def update(self, setpoint, current, dt):
23 | error = setpoint-current
24 | derivative = (error-self.last_error)/dt
25 | self.integral += error * dt
26 | output = (self.Kp * error) + (self.Ki * self.integral) + (self.Kd * derivative)
27 | self.last_error = error
28 | output = max(min(output,100),-100)
29 | return output
30 |
31 | MasterLevelController = None
32 | FeedA = None
33 | FeedB = None
34 | fw_valve = 0
35 | FeedAValve = 0
36 | FeedBValve = 0
37 | setpoint_setdown_mode = 0 #-2 Inactive | -1 Active | >= 0 Timing
38 | #monitoring = None
39 |
40 | def initialize():
41 | #initialize our PIDs:
42 | global MasterLevelController
43 | MasterLevelController = PID(Kp=0.12, Ki=0, Kd=0.5)
44 | #DT is DeltaTime (just use 1 for now)
45 | global FeedA
46 | FeedA = PID(Kp=0.12, Ki=0, Kd=0.5)
47 |
48 | global FeedB
49 | FeedB = PID(Kp=0.12, Ki=0, Kd=0.5)
50 |
51 | #global monitoring
52 | #monitoring = transient.Transient("Reactor Parameters")
53 | #monitoring.add_graph("RX POWER")
54 | #monitoring.add_graph("RX LEVEL")
55 | #monitoring.add_graph("RX PRESSURE")
56 |
57 | rft_a_trip = False
58 | rft_b_trip = False
59 | rft_a_reset_timer = -1
60 | rft_b_reset_timer = -1
61 | tng_timer_a = -1
62 | tnga = False #Turning gear A
63 | tng_timer_b = -1
64 | tngb = False #Turning gear B
65 |
66 | def run():
67 |
68 | global requested_setpoint
69 | global actual_setpoint
70 |
71 | global setpoint_setdown_mode
72 |
73 | trip_a = (reactor_protection_system.reactor_protection_systems["A"]["channel_1_trip"] or reactor_protection_system.reactor_protection_systems["A"]["channel_2_trip"])
74 |
75 | trip_b = (reactor_protection_system.reactor_protection_systems["B"]["channel_1_trip"] or reactor_protection_system.reactor_protection_systems["B"]["channel_2_trip"])
76 |
77 | scram_signal = trip_a and trip_b
78 |
79 | #Setpoint setdown reduces setpoint by 18" following a scram. It is active during the whole scram.
80 | #After a scram reset, the setpoint ramps up 18 inches over a 9 minute time period
81 | if scram_signal:
82 | setpoint_setdown_mode = -1
83 |
84 | if (not scram_signal) and setpoint_setdown_mode == -1:
85 | setpoint_setdown_mode = 5400
86 |
87 | if setpoint_setdown_mode > 0:
88 | setpoint_setdown_mode -= 1
89 |
90 | if setpoint_setdown_mode == 0:
91 | setpoint_setdown_mode = -2
92 |
93 | model.alarms["setpoint_setdown_active"]["alarm"] = setpoint_setdown_mode != -2
94 |
95 | if setpoint_setdown_mode >= 0:
96 | actual_setpoint = requested_setpoint-(18*(setpoint_setdown_mode/5400))
97 | elif setpoint_setdown_mode == -1:
98 | actual_setpoint = requested_setpoint-18
99 | else:
100 | actual_setpoint = requested_setpoint
101 |
102 | global fw_valve
103 |
104 | control_signal = MasterLevelController.update(actual_setpoint,reactor_inventory.rx_level_wr,1)
105 |
106 | fw_valve = max(min(fw_valve+control_signal,100),0)
107 |
108 | #TODO: Temporarily using the RFW Isolation valves as control valves
109 | fluid.valves["rfw_v_65a"]["percent_open"] = fw_valve
110 | fluid.valves["rfw_v_65b"]["percent_open"] = fw_valve
111 |
112 | global FeedAValve
113 | global FeedBValve
114 |
115 | #RFT Stop & Trip system
116 |
117 | global rft_a_trip
118 | global rft_b_trip
119 | global rft_a_reset_timer
120 | global rft_b_reset_timer
121 |
122 | if model.pumps["rfw_p_1a"]["rpm"] > 6100:
123 | model.alarms["turbine_a_overspeed_trip"]["alarm"] = True
124 | rft_a_trip = True
125 |
126 | if model.pumps["rfw_p_1b"]["rpm"] > 6100:
127 | model.alarms["turbine_b_overspeed_trip"]["alarm"] = True
128 | rft_b_trip = True
129 |
130 | if model.switches["rft_dt_1a_trip"]["position"] == 0:
131 | rft_a_trip = True
132 | elif model.switches["rft_dt_1a_trip"]["position"] == 2 and rft_a_reset_timer == -1:
133 | rft_a_reset_timer = 600 #TODO: Actually simulate this?
134 | model.alarms["turbine_a_overspeed_trip"]["alarm"] = False
135 | elif model.switches["rft_dt_1a_trip"]["position"] == 2 and rft_a_reset_timer > 0:
136 | rft_a_reset_timer -= 1
137 | elif model.switches["rft_dt_1a_trip"]["position"] == 2 and rft_a_reset_timer <= 0:
138 | rft_a_trip = False
139 | rft_a_reset_timer = -1
140 |
141 | if model.switches["rft_dt_1b_trip"]["position"] == 0:
142 | rft_b_trip = True
143 | elif model.switches["rft_dt_1b_trip"]["position"] == 2 and rft_b_reset_timer == -1:
144 | rft_b_reset_timer = 600 #TODO: Actually simulate this?
145 | model.alarms["turbine_b_overspeed_trip"]["alarm"] = False
146 | elif model.switches["rft_dt_1b_trip"]["position"] == 2 and rft_b_reset_timer > 0:
147 | rft_b_reset_timer -= 1
148 | elif model.switches["rft_dt_1b_trip"]["position"] == 2 and rft_b_reset_timer <= 0:
149 | rft_b_trip = False
150 | rft_b_reset_timer = -1
151 |
152 | #set the state of the tripe!!!!
153 | model.alarms["turbine_a_tripped"]["alarm"] = rft_a_trip
154 | if rft_a_trip:
155 | fluid.valves["ms_v_172a"]["percent_open"] = min(max(fluid.valves["ms_v_172a"]["percent_open"]-25,0),100)
156 | else:
157 | fluid.valves["ms_v_172a"]["percent_open"] = min(max(fluid.valves["ms_v_172a"]["percent_open"]+25,0),100)
158 |
159 | model.alarms["turbine_b_tripped"]["alarm"] = rft_b_trip
160 | if rft_b_trip:
161 | fluid.valves["ms_v_172b"]["percent_open"] = min(max(fluid.valves["ms_v_172b"]["percent_open"]-25,0),100)
162 | else:
163 | fluid.valves["ms_v_172b"]["percent_open"] = min(max(fluid.valves["ms_v_172b"]["percent_open"]+25,0),100)
164 |
165 | #Turning gears come on 10 seconds after HP and LPs come closed, RFT speed is LT 1 rpm, switch is in AUTO, and bearing lube oil GT 5 psig
166 | #Switches maintained in OFF during operation to prevent actuation during operation, and put in AUTO after RFT trip
167 |
168 | global tng_timer_a
169 | global tng_timer_b
170 | global tnga
171 | global tngb
172 |
173 | if fluid.valves["ms_v_172a"]["percent_open"] <= 0: #TODO: LP system
174 | if tng_timer_a == -1:
175 | tng_timer_a = 100
176 | elif tng_timer_a > 0:
177 | tng_timer_a -= 1
178 | else:
179 | tng_timer_a = -1
180 |
181 | if model.switches["rft_tng_1a"]["position"] == 1 and tng_timer_a <= 0 and model.pumps["rfw_p_1a"]["rpm"] < 1: #sw in auto
182 | tnga = True
183 |
184 | if model.switches["rft_tng_1a"]["position"] == 0:
185 | tnga = False
186 |
187 | if tnga:
188 | model.pumps["rfw_p_1a"]["rpm"] = 20
189 |
190 | model.switches["rft_tng_1a"]["lights"]["green"] = not tnga
191 | model.switches["rft_tng_1a"]["lights"]["red"] = tnga
192 |
193 | if fluid.valves["ms_v_172b"]["percent_open"] <= 0: #TODO: LP system
194 | if tng_timer_b == -1:
195 | tng_timer_b = 100
196 | elif tng_timer_b > 0:
197 | tng_timer_b -= 1
198 | else:
199 | tng_timer_b = -1
200 |
201 | if model.switches["rft_tng_1b"]["position"] == 1 and tng_timer_b <= 0 and model.pumps["rfw_p_1b"]["rpm"] < 1: #sw in auto
202 | tngb = True
203 |
204 | if model.switches["rft_tng_1b"]["position"] == 0:
205 | tngb = False
206 |
207 | if tngb:
208 | model.pumps["rfw_p_1b"]["rpm"] = 20
209 |
210 | model.switches["rft_tng_1b"]["lights"]["green"] = not tngb
211 | model.switches["rft_tng_1b"]["lights"]["red"] = tngb
212 |
213 |
214 |
215 |
216 | #RFT Governors
217 | control_signal_rfta = FeedA.update(5000,model.pumps["rfw_p_1a"]["rpm"],1)
218 |
219 | FeedAValve = max(min(FeedAValve+control_signal_rfta,100),0)
220 |
221 | control_signal_rftb = FeedB.update(5000,model.pumps["rfw_p_1b"]["rpm"],1)
222 |
223 | FeedBValve = max(min(FeedBValve+control_signal_rftb,100),0)
224 |
225 | fluid.valves["rft_gov_1a"]["percent_open"] = FeedAValve
226 | fluid.valves["rft_gov_1b"]["percent_open"] = FeedBValve
227 |
228 | model.values["rfw_rpv_inlet_pressure"] = fluid.headers["rfw_discharge"]["pressure"]/6895
229 |
230 | model.values["rft_dt_1a_rpm"] = model.pumps["rfw_p_1a"]["rpm"]
231 | model.values["rft_dt_1b_rpm"] = model.pumps["rfw_p_1b"]["rpm"]
232 |
233 |
234 |
235 |
236 |
237 |
238 |
239 |
240 |
241 |
242 |
243 |
244 |
245 |
246 |
247 |
248 |
249 |
250 |
251 |
252 |
253 |
254 |
255 |
256 |
257 | #monitoring.add("RX LEVEL",reactor_inventory.rx_level_wr)
258 | #monitoring.add("RX PRESSURE",pressure.Pressures["Vessel"]/6895)
259 | #monitoring.add("RX POWER",neutron_monitoring.average_power_range_monitors["A"]["power"])
260 |
261 | #valueee = False
262 | #a = 1
263 | #if valueee == True:
264 | #monitoring.generate_plot()
265 |
266 |
267 |
268 |
269 |
270 |
271 |
272 |
273 |
--------------------------------------------------------------------------------
/simulation/models/control_room_columbia/systems/fire.py:
--------------------------------------------------------------------------------
1 | from simulation.models.control_room_columbia.general_physics import ac_power
2 | import random
3 |
4 | def DamageEquipmentTGElect471(fire_time):
5 | breakers_allowed = ["cb_s1","cb_s2","cb_s3"]
6 | if fire_time > 100:
7 | for breaker_name in ac_power.breakers:
8 | if breaker_name in breakers_allowed:
9 | if random.randint(0,2000) == 4:
10 | ac_power.breakers[breaker_name].info["lockout"] = True
11 | ac_power.breakers[breaker_name].open()
12 |
13 | class SLC():
14 | def __init__(self,uid):
15 | self.uid = uid
16 | self.ground = False
17 | self.trouble = False
18 | self.fire = False
19 | self.areas = {}
20 |
21 | SLCs[self.uid] = self
22 |
23 | def add_detector(self,area_name):
24 | if area_name not in self.areas:
25 | self.areas[area_name] = False #Allows proper annunciation of "general area" and whatnot
26 |
27 | def detector_triggered(self,area_name):
28 | self.fire = True
29 | self.areas[area_name] = True
30 |
31 | def detector_untriggered(self,area_name):
32 | self.areas[area_name] = False
33 |
34 | class Detector_Type_Photo:
35 | def __init__(self,uid,slc,area):
36 | self.uid = uid
37 | self.sensitivity = 3 #percent of smoke in the area
38 | self.slc = slc #SLC loop it is attatched to
39 | self.area = area
40 |
41 | slc.add_detector(self.area.name)
42 |
43 | detectors.append(self)
44 |
45 | def heartbeat(self):
46 | area_temperature = self.area.temperature
47 | area_smoke = self.area.smoke_content
48 |
49 | if area_smoke > self.sensitivity:
50 | #go into alarm
51 | self.slc.detector_triggered(self.area.name)
52 | else:
53 | self.slc.detector_untriggered(self.area.name)
54 |
55 | class Suppression_Type_Wet:
56 | def __init__(self,uid,slc,area,head_shatter,head_flowrate):
57 | self.uid = uid
58 | self.slc = slc
59 | self.area = area
60 | self.head_shatter = head_shatter
61 | self.head_flowrate = head_flowrate
62 | self.shattered = False
63 |
64 | self.slc.add_detector(self.area.name+"_FLOW")
65 |
66 | suppression[self.uid] = self
67 |
68 | def heartbeat(self):
69 | area_temperature = self.area.temperature
70 |
71 | if area_temperature > self.head_shatter:
72 | self.shattered = True
73 |
74 | if self.shattered:
75 | self.slc.detector_triggered(self.area.name+"_FLOW")
76 | #begin suppression flow
77 |
78 | #TODO:use actual flow calculations
79 | pressure = 100 #TODO
80 | flow = self.head_flowrate * (pressure/100)
81 |
82 | #TODO: add fire header
83 |
84 | effectiveness = flow/111
85 |
86 | for fire in self.area.fires:
87 | fire.suppress(effectiveness)
88 |
89 |
90 |
91 |
92 | class Area:
93 | def __init__(self,name,volume):
94 | self.name = name
95 | self.volume = volume
96 | self.temperature = 20
97 | self.smoke_content = 0 #perecent
98 | self.fires = []
99 | self.suppression = None #A area can only have one suppression device
100 | self.equipment = []
101 |
102 | areas[name] = self
103 |
104 | def add_equipment(self,func):
105 | self.equipment.append(func)
106 |
107 |
108 | class Fire_Type_Paper: #Easy to extinguish, moderate smoke
109 | def __init__(self,area,intensity):
110 | self.set_intensity = intensity
111 | self.intensity = intensity #TODO: Make this come up to set_intensity over time
112 | self.area = area
113 | self.spread_chance = 0 #Goes up over time as the fire is not extinguished.
114 | self.put_out_effectiveness = 0.05 #very easy to extinguish
115 | self.fire_time = 0 #seconds
116 |
117 | fires.append(self)
118 | self.area.fires.append(self)
119 |
120 | def spread_to_areas(self):
121 | print("Spread")
122 |
123 | def put_out(self):
124 | self.area.fires.remove(self)
125 | fires.remove(self)
126 | print("%s fire is out." % self.area.name)
127 |
128 | def suppress(self,effectiveness):
129 | suppression = effectiveness/self.put_out_effectiveness
130 |
131 | if suppression > 1:
132 | self.put_out()
133 | else:
134 | self.intensity -= self.intensity*suppression*0.01
135 | self.intensity = max(self.intensity,0.1)
136 | self.put_out_effectiveness = self.intensity*0.95
137 |
138 | def calculate(self,delta):
139 | self.fire_time += delta
140 | heat = 180*(self.intensity+0.15)
141 | self.area.temperature += (heat-self.area.temperature)*0.01
142 | self.area.smoke_content += 0.1*self.intensity
143 |
144 | self.area.smoke_content = min(max(self.area.smoke_content,0),100)
145 |
146 | for equip in self.area.equipment:
147 | equip(self.fire_time)
148 |
149 | class Fire_Type_Electrical: #Hard to extinguish, moderate smoke, requires de-energization(?)
150 | def __init__(self,area,intensity):
151 | self.set_intensity = intensity
152 | self.intensity = intensity #TODO: Make this come up to set_intensity over time
153 | self.area = area
154 | self.spread_chance = 0 #Goes up over time as the fire is not extinguished.
155 | self.put_out_effectiveness = 0.7
156 | self.fire_time = 0 #seconds
157 |
158 | fires.append(self)
159 | self.area.fires.append(self)
160 |
161 | def spread_to_areas(self):
162 | print("Spread")
163 |
164 | def put_out(self):
165 | self.area.fires.remove(self)
166 | fires.remove(self)
167 | print("%s fire is out." % self.area.name)
168 |
169 | def suppress(self,effectiveness):
170 | suppression = effectiveness/self.put_out_effectiveness
171 |
172 | if suppression > 1:
173 | self.put_out()
174 | else:
175 | self.intensity -= self.intensity*suppression*0.01
176 | self.intensity = max(self.intensity,0.2)
177 | self.put_out_effectiveness = self.intensity*0.95
178 |
179 | def calculate(self,delta):
180 | self.fire_time += delta
181 | heat = 200*(self.intensity+0.15)
182 | self.area.temperature += (heat-self.area.temperature)*0.01
183 | self.area.smoke_content += 0.1*self.intensity
184 |
185 | self.area.smoke_content = min(max(self.area.smoke_content,0),100)
186 |
187 | for equip in self.area.equipment:
188 | equip(self.fire_time)
189 |
190 | detectors = []
191 | fires = []
192 | SLCs = {}
193 | areas = {}
194 | suppression = {}
195 |
196 | def initialize():
197 | tg = Area("TG BLDG 471 ELECT SWGR",100)
198 | tg.add_equipment(DamageEquipmentTGElect471)
199 | tgslc = SLC("TG BLDG 471 ELECT SWGR")
200 | tgslcsp = SLC("SYS 7 WET PIPE TG BLDG 471 ELECT SWGR")
201 | Detector_Type_Photo("PHOTO TG 471", tgslc,tg)
202 | Suppression_Type_Wet("SYS 7 WET PIPE TG BLDG 471 ELECT SWGR",tgslcsp,tg,93,11)
203 | #Fire_Type_Electrical(tg,0.4)
204 |
205 | def run(delta):
206 | for detector in detectors:
207 | detector.heartbeat()
208 |
209 | for system in suppression:
210 | system = suppression[system]
211 | system.heartbeat()
212 |
213 | for fire in fires:
214 | fire.calculate(delta)
--------------------------------------------------------------------------------
/simulation/models/control_room_columbia/systems/fire_control_panel.py:
--------------------------------------------------------------------------------
1 | from simulation.models.control_room_columbia import model
2 | from simulation.models.control_room_columbia.systems import fire
3 |
4 | def run(delta):
5 | model.alarms["tg_bldg_471_elect_swgr_bay"]["alarm"] = fire.SLCs["TG BLDG 471 ELECT SWGR"].fire or fire.SLCs["TG BLDG 471 ELECT SWGR"].trouble
6 | model.alarms["tg_bldg_471_elect_swgr_bay_trouble"]["alarm"] = fire.SLCs["TG BLDG 471 ELECT SWGR"].trouble
7 | model.alarms["tg_bldg_471_elect_swgr_bay_fire"]["alarm"] = fire.SLCs["TG BLDG 471 ELECT SWGR"].fire
8 |
9 | model.alarms["sys_7_wet_pipe_tg_bldg_471_elect_swgr_bay"]["alarm"] = fire.SLCs["SYS 7 WET PIPE TG BLDG 471 ELECT SWGR"].fire or fire.SLCs["SYS 7 WET PIPE TG BLDG 471 ELECT SWGR"].trouble
10 | model.alarms["sys_7_wet_pipe_tg_bldg_471_elect_swgr_bay_trouble"]["alarm"] = fire.SLCs["SYS 7 WET PIPE TG BLDG 471 ELECT SWGR"].trouble
11 | model.alarms["sys_7_wet_pipe_tg_bldg_471_elect_swgr_bay_fire"]["alarm"] = fire.SLCs["SYS 7 WET PIPE TG BLDG 471 ELECT SWGR"].fire
--------------------------------------------------------------------------------
/simulation/models/control_room_columbia/systems/fukushima.py:
--------------------------------------------------------------------------------
1 | from simulation.models.control_room_columbia import model
2 | from simulation.models.control_room_columbia import reactor_protection_system
3 | from simulation.constants.annunciator_states import AnnunciatorStates
4 |
5 | def run(runs):
6 | if runs >= 200:
7 | model.indicators["cr_light_normal"] = False
8 | model.indicators["cr_light_emergency"] = True
9 |
10 | if runs >= 230:
11 | reactor_protection_system.reactor_protection_systems["A"]["channel_1_trip"] = True
12 | reactor_protection_system.reactor_protection_systems["A"]["channel_2_trip"] = True
13 |
14 | if runs > 235:
15 | reactor_protection_system.reactor_protection_systems["B"]["channel_1_trip"] = True
16 | reactor_protection_system.reactor_protection_systems["B"]["channel_2_trip"] = True
17 |
18 | if runs >= 320:
19 | model.indicators["cr_light_normal"] = True
20 | model.indicators["cr_light_emergency"] = False
21 |
22 | if runs >= 390:
23 | model.indicators["cr_light_normal"] = False
24 | model.indicators["cr_light_emergency"] = True
25 | model.indicators["CHART_RECORDERS_OPERATE"] = False
26 |
27 | if runs > 420:
28 | model.indicators["cr_light_emergency"] = False
29 |
30 | if runs > 450:
31 | for alarm in model.alarms:
32 | model.alarms[alarm]["alarm"] = False
33 | model.alarms[alarm]["state"] = AnnunciatorStates.CLEAR
34 |
35 | model.indicators["SCRAM_A5"] = False
36 | model.indicators["SCRAM_A6"] = False
37 | model.indicators["SCRAM_B5"] = False
38 | model.indicators["SCRAM_B6"] = False
39 |
40 | if runs > 470:
41 | model.indicators["FCD_OPERATE"] = False
42 |
43 |
44 |
--------------------------------------------------------------------------------
/simulation/models/control_room_columbia/systems/hpcs.py:
--------------------------------------------------------------------------------
1 | from simulation.models.control_room_columbia import model
2 | from simulation.models.control_room_columbia.reactor_physics import reactor_inventory
3 | from simulation.models.control_room_columbia.general_physics import fluid
4 |
5 | hpcs_init = False
6 | hpcs_init_first = False
7 |
8 | hpcs_l8 = False
9 |
10 |
11 | def run():
12 |
13 | global hpcs_init
14 | global hpcs_init_first
15 | global hpcs_l8
16 |
17 | model.values["hpcs_flow"] = model.pumps["hpcs_p_1"]["actual_flow"]
18 | model.values["hpcs_press"] = fluid.headers["hpcs_discharge_header"]["pressure"]/6895
19 |
20 | model.alarms["hpcs_actuated"]["alarm"] = hpcs_init
21 |
22 | if reactor_inventory.rx_level_wr < -51:
23 | if hpcs_init == False:
24 | hpcs_init = True
25 | hpcs_init_first = True
26 |
27 | #Open HPCS-V-4 to allow cycling between L2 and L8
28 | fluid.valves["hpcs_v_4"]["sealed_in"] = True
29 |
30 | if reactor_inventory.rx_level_wr > 54:
31 | hpcs_l8 = True
32 | #Close HPCS-V-4
33 | fluid.valves["hpcs_v_4"]["sealed_in"] = False
34 |
35 | if model.pumps["hpcs_p_1"]["motor_breaker_closed"]:
36 | fluid.valves["hpcs_v_12"]["sealed_in"] = model.pumps["hpcs_p_1"]["actual_flow"] < 1200
37 |
38 | if model.buttons["hpcs_init"]["state"]:
39 | hpcs_init = True
40 | hpcs_init_first = True
41 | fluid.valves["hpcs_v_4"]["sealed_in"] = True
42 |
43 | if model.buttons["hpcs_init_reset"]["state"] and reactor_inventory.rx_level_wr > -51:
44 | hpcs_init = False
45 | hpcs_init_first = False
46 |
47 | if model.buttons["hpcs_l8_reset"]["state"] and reactor_inventory.rx_level_wr < 54:
48 | hpcs_l8 = False
49 |
50 | if hpcs_init:
51 | if hpcs_init_first:
52 | model.pumps["hpcs_p_1"]["motor_breaker_closed"] = True
53 | hpcs_init_first = False
54 |
55 | model.indicators["hpcs_init"] = hpcs_init
56 | model.indicators["hpcs_l8"] = hpcs_l8
--------------------------------------------------------------------------------
/simulation/models/control_room_columbia/systems/irm_srm_positioner.py:
--------------------------------------------------------------------------------
1 | from simulation.models.control_room_columbia import model
2 | from simulation.models.control_room_columbia import neutron_monitoring
3 |
4 | srm_selected = []
5 | irm_selected = []
6 | currently_pressed = []
7 | drive_in_latch = False
8 | drive_in_pressed = False
9 |
10 | def run():
11 | global srm_selected
12 | global irm_selected
13 | global currently_pressed
14 | global drive_in_latch
15 | global drive_in_pressed
16 | for srm_name in neutron_monitoring.source_range_monitors:
17 | if not "SELECT_SRM_%s" % srm_name in model.buttons:
18 | continue
19 |
20 | model.indicators["SELECT_SRM_%s" % srm_name] = srm_name in srm_selected
21 |
22 | button = model.buttons["SELECT_SRM_%s" % srm_name]
23 | if button["state"] and (not srm_name in currently_pressed):
24 | if srm_name in srm_selected:
25 | srm_selected.remove(srm_name)
26 | else:
27 | srm_selected.append(srm_name)
28 | currently_pressed.append(srm_name)
29 | elif (not button["state"]) and srm_name in currently_pressed:
30 | currently_pressed.remove(srm_name)
31 |
32 | model.indicators["SRM_%s_POS_OUT" % srm_name] = neutron_monitoring.source_range_monitors[srm_name]["withdrawal_percent"] >= 100
33 | model.indicators["SRM_%s_POS_IN" % srm_name] = neutron_monitoring.source_range_monitors[srm_name]["withdrawal_percent"] <= 0
34 |
35 |
36 | for irm_name in neutron_monitoring.intermediate_range_monitors:
37 | if "SELECT_IRM_%s" % irm_name not in model.buttons:
38 | continue
39 |
40 | button = model.buttons["SELECT_IRM_%s" % irm_name]
41 |
42 | model.indicators["SELECT_IRM_%s" % irm_name] = irm_name in irm_selected
43 |
44 | if button["state"] and (not irm_name in currently_pressed):
45 | if irm_name in irm_selected:
46 | irm_selected.remove(irm_name)
47 | else:
48 | irm_selected.append(irm_name)
49 | currently_pressed.append(irm_name)
50 | elif (not button["state"]) and irm_name in currently_pressed:
51 | currently_pressed.remove(irm_name)
52 |
53 | model.indicators["IRM_%s_POS_OUT" % irm_name] = neutron_monitoring.intermediate_range_monitors[irm_name]["withdrawal_percent"] >= 100
54 | model.indicators["IRM_%s_POS_IN" % irm_name] = neutron_monitoring.intermediate_range_monitors[irm_name]["withdrawal_percent"] <= 0
55 |
56 | if model.buttons["DET_DRIVE_IN"]["state"] and not drive_in_pressed:
57 | drive_in_latch = not drive_in_latch
58 |
59 | drive_in_pressed = model.buttons["DET_DRIVE_IN"]["state"]
60 |
61 |
62 | model.indicators["DET_DRIVE_IN"] = drive_in_latch
63 | model.indicators["DET_DRIVE_OUT"] = model.buttons["DET_DRIVE_OUT"]["state"]
64 | if drive_in_latch:
65 |
66 | for detector in srm_selected:
67 | srm = neutron_monitoring.source_range_monitors[detector]
68 | srm["withdrawal_percent"] = min(max(srm["withdrawal_percent"]-0.0416,0),100)
69 |
70 | for detector in irm_selected:
71 | irm = neutron_monitoring.intermediate_range_monitors[detector]
72 | irm["withdrawal_percent"] = min(max(irm["withdrawal_percent"]-0.0416,0),100)
73 |
74 | elif model.buttons["DET_DRIVE_OUT"]["state"]:
75 | for detector in srm_selected:
76 | srm = neutron_monitoring.source_range_monitors[detector]
77 | srm["withdrawal_percent"] = min(max(srm["withdrawal_percent"]+0.0416,0),100)
78 |
79 | for detector in irm_selected:
80 | irm = neutron_monitoring.intermediate_range_monitors[detector]
81 | irm["withdrawal_percent"] = min(max(irm["withdrawal_percent"]+0.0416,0),100)
--------------------------------------------------------------------------------
/simulation/models/control_room_columbia/systems/loop_sequence.py:
--------------------------------------------------------------------------------
1 | from simulation.models.control_room_columbia.general_physics import ac_power
2 | from simulation.models.control_room_columbia import model
3 |
4 | class Sequence():
5 |
6 | def __init__(self,bus_name):
7 | self.bus_name = bus_name
8 | self.loads = {}
9 | self.sequence_enabled = False #Occurs on a Loss Of Power
10 | self.sequence_timer = -1
11 | self.highest_time = 0
12 |
13 | sequences[bus_name] = self
14 |
15 | def add_pump(self,name,time):
16 | """Adds a pump to be sequenced on, in seconds"""
17 | time = time*10
18 | self.loads[name] = time
19 |
20 | self.highest_time = max(self.highest_time,time)
21 |
22 | model.pumps[name]["loop_seq"] = True
23 |
24 | def calculate(self):
25 |
26 | no_voltage = not ac_power.busses[self.bus_name].is_voltage_at_bus(120) #at LEAST 120 volts at the bus
27 |
28 | if no_voltage:
29 | self.sequence_enabled = True
30 | self.sequence_timer = -1
31 |
32 | if not no_voltage and self.sequence_enabled:
33 | if self.sequence_timer == -1:
34 | self.sequence_timer = 0
35 | elif self.sequence_timer < self.highest_time:
36 | self.sequence_timer += 1
37 | else:
38 | self.sequence_timer = -1
39 | self.sequence_enabled = False
40 |
41 | for load in self.loads:
42 | model.pumps[load]["loop_avail"] = self.sequence_timer >= self.loads[load] or not self.sequence_enabled
43 |
44 |
45 |
46 |
47 |
48 |
49 | sequences = {}
50 |
51 | def initialize():
52 | Sequence("7")
53 | sequences["7"].add_pump("lpcs_p_1",0)
54 | sequences["7"].add_pump("rhr_p_2a",5)
55 |
56 | Sequence("8")
57 | sequences["8"].add_pump("rhr_p_2c",0)
58 | sequences["8"].add_pump("rhr_p_2b",5)
59 |
60 | def run():
61 | for sequence in sequences:
62 | sequence = sequences[sequence]
63 | sequence.calculate()
--------------------------------------------------------------------------------
/simulation/models/control_room_columbia/systems/lpcs.py:
--------------------------------------------------------------------------------
1 | from simulation.models.control_room_columbia import model
2 | from simulation.models.control_room_columbia.reactor_physics import reactor_inventory
3 | from simulation.models.control_room_columbia.general_physics import fluid
4 | from simulation.models.control_room_columbia.systems import residual_heat_removal
5 |
6 | def run():
7 |
8 | model.values["lpcs_flow"] = model.pumps["lpcs_p_1"]["actual_flow"]
9 | model.values["lpcs_press"] = fluid.headers["lpcs_discharge_header"]["pressure"]/6895
10 |
11 | model.alarms["lpcs_rhr_a_actuated"]["alarm"] = residual_heat_removal.rhr_a_lpcs_init
12 |
13 | if residual_heat_removal.rhr_a_lpcs_init:
14 | if residual_heat_removal.rhr_a_lpcs_init_first:
15 | model.pumps["lpcs_p_1"]["motor_breaker_closed"] = True
16 | residual_heat_removal.rhr_a_lpcs_init_first = False
17 |
18 | fluid.valves["lpcs_v_5"]["sealed_in"] = True
19 |
20 |
21 | if model.pumps["lpcs_p_1"]["motor_breaker_closed"]:
22 | fluid.valves["lpcs_v_11"]["sealed_in"] = model.pumps["lpcs_p_1"]["actual_flow"] < 1200
23 |
24 | if model.buttons["lpcs_man_init"]["state"]:
25 | residual_heat_removal.rhr_a_lpcs_init = True
26 | residual_heat_removal.rhr_a_lpcs_init_first = True
27 |
28 | if model.buttons["lpcs_init_reset"]["state"] and reactor_inventory.rx_level_wr > -129:
29 | residual_heat_removal.rhr_a_lpcs_init = False
30 | residual_heat_removal.rhr_a_lpcs_init_first = False
31 | model.alarms["lpcs_rhr_a_init_rpv_level_low"]["alarm"] = False
32 |
33 | #automatic init RPV level low -129"
34 | if reactor_inventory.rx_level_wr <= -129:
35 | if residual_heat_removal.rhr_a_lpcs_init == False:
36 | residual_heat_removal.rhr_a_lpcs_init = True
37 | residual_heat_removal.rhr_a_lpcs_init_first = True
38 |
39 | model.alarms["lpcs_rhr_a_init_rpv_level_low"]["alarm"] = True
40 |
41 | model.indicators["lpcs_init"] = residual_heat_removal.rhr_a_lpcs_init
--------------------------------------------------------------------------------
/simulation/models/control_room_columbia/systems/rcic.py:
--------------------------------------------------------------------------------
1 | from simulation.models.control_room_columbia import model
2 | from simulation.models.control_room_columbia.reactor_physics import reactor_inventory
3 | from simulation.models.control_room_columbia.general_physics import fluid
4 | from simulation.models.control_room_columbia.systems import ESFAS
5 |
6 | trip_permissive_delay = -1
7 | start_delay = -1
8 | initiation = False
9 | high_level_shutdown = False
10 |
11 | def run():
12 |
13 | model.values["rcic_flow"] = model.pumps["rcic_p_1"]["actual_flow"]
14 |
15 | model.values["rcic_rpm"] = model.turbines["rcic_turbine"]["rpm"]
16 | model.values["rcic_supply_press"] = fluid.headers["rcic_main_steam_line"]["pressure"]/6895
17 | model.values["rcic_exhaust_press"] = fluid.headers["rcic_exhaust_steam_line"]["pressure"]/6895
18 |
19 | model.values["rcic_pump_suct_press"] = fluid.headers["rcic_suction_header"]["pressure"]/6895
20 | model.values["rcic_pump_disch_press"] = fluid.headers["rcic_discharge_header"]["pressure"]/6895
21 |
22 | if fluid.headers["rcic_exhaust_steam_line"]["pressure"]/6895 >= 30: #tech specs 3.3.6.1
23 | model.turbines["rcic_turbine"]["trip"] = True #this trips the RCIC turbine, and if the rupture disk blows, RCIC isolates
24 |
25 | global start_delay
26 | global initiation
27 | global high_level_shutdown
28 |
29 | low_level_init = False
30 |
31 | if reactor_inventory.rx_level_wr < ESFAS.RPV_LEVEL_2_in:
32 | high_level_shutdown = False
33 | low_level_init = True
34 |
35 |
36 | if reactor_inventory.rx_level_wr > ESFAS.RPV_LEVEL_8_in:
37 | high_level_shutdown = True
38 |
39 |
40 | #FSAR states RCIC-V-45 being NOT full closed initiates a 15 second time delay for the low suction and discharge pressure trip and;
41 | #initiates the startup ramp
42 |
43 | #Operator stated that the signal must be in for six seconds until the valves will begin to reposition. (Seal in light wont come in until that happens)
44 |
45 | #forces operator to depress for six seconds until the signal comes in
46 | if initiation or start_delay == 0 or model.buttons["rcic_init"]["state"] or low_level_init:
47 | model.alarms["rcic_actuated"]["alarm"] = True #should this come in?
48 | if start_delay == -1:
49 | start_delay = 60
50 | elif start_delay > 0:
51 | start_delay -= 1
52 | elif start_delay== 0:
53 | model.indicators["rcic_init"] = True
54 | #initiation signal opens;
55 | # RCIC-V-45 and RCIC-V-46 and;
56 | # RCIC-V-13 to open, RCIC-V-13,RCIC-V-13,RCIC-V-13,RCIC-V-13 to close, SW-P-1B to start, and RRA-FN-6 to start (permissive on RCIC-V-1 and RCIC-V-45 being open) and;
57 | # starts the barometric condenser vacuum pump
58 | fluid.valves["rcic_v_45"]["sealed_in"] = not high_level_shutdown
59 | if fluid.valves["rcic_v_45"]["percent_open"] != 0 and fluid.valves["rcic_v_1"]["percent_open"] != 0:
60 | fluid.valves["rcic_v_13"]["sealed_in"] = True
61 | else:
62 | start_delay = -1
63 | model.alarms["rcic_actuated"]["alarm"] = False
64 | model.indicators["rcic_init"] = False
65 |
66 | if model.buttons["rcic_init_reset"]["state"] and (initiation == False and model.buttons["rcic_init"]["state"] == False):
67 | start_delay = -1
68 |
69 | model.alarms["rcic_turbine_trip"]["alarm"] = model.turbines["rcic_turbine"]["trip"]
70 |
71 |
72 | global trip_permissive_delay
73 |
74 | if model.switches["rcic_v_1"]["position"] == 0 and fluid.valves["rcic_v_1"]["percent_open"] <= 0:
75 | model.turbines["rcic_turbine"]["trip"] = False
76 |
77 | if fluid.valves["rcic_v_45"]["percent_open"] != 0 and trip_permissive_delay == -1:
78 | trip_permissive_delay = 150
79 | elif fluid.valves["rcic_v_45"]["percent_open"] != 0 and trip_permissive_delay > 0:
80 | trip_permissive_delay -= 1
81 | elif fluid.valves["rcic_v_45"]["percent_open"] != 0 and trip_permissive_delay == 0:
82 | #trips
83 | if fluid.headers["rcic_discharge_header"]["pressure"]/6895 < 400: #TODO: find actual trip setpoint
84 | model.turbines["rcic_turbine"]["trip"] = True
85 |
86 | if fluid.headers["rcic_discharge_header"]["pressure"]/6895 < 20: #TODO: find actual trip setpoint
87 | model.turbines["rcic_turbine"]["trip"] = True
88 | else:
89 | trip_permissive_delay = -1
90 |
91 |
92 |
--------------------------------------------------------------------------------
/simulation/models/control_room_columbia/systems/residual_heat_removal.py:
--------------------------------------------------------------------------------
1 | from simulation.models.control_room_columbia import model
2 | from simulation.models.control_room_columbia.reactor_physics import reactor_inventory
3 | from simulation.models.control_room_columbia.general_physics import fluid
4 |
5 | rhr_a_lpcs_init = False
6 | rhr_a_lpcs_init_first = False
7 |
8 | rhr_cb_init = False
9 | rhr_cb_init_first = False
10 |
11 | def run():
12 |
13 | global rhr_a_lpcs_init
14 | global rhr_a_lpcs_init_first
15 |
16 | global rhr_cb_init
17 | global rhr_cb_init_first
18 |
19 | model.values["rhr_a_flow"] = model.pumps["rhr_p_2a"]["actual_flow"]
20 | model.values["rhr_a_press"] = fluid.headers["rhr_a_discharge_header"]["pressure"]/6895
21 |
22 | model.values["rhr_b_flow"] = model.pumps["rhr_p_2b"]["actual_flow"]
23 | model.values["rhr_b_press"] = fluid.headers["rhr_b_discharge_header"]["pressure"]/6895
24 |
25 | model.values["rhr_c_flow"] = model.pumps["rhr_p_2c"]["actual_flow"]
26 | model.values["rhr_c_press"] = fluid.headers["rhr_c_discharge_header"]["pressure"]/6895
27 |
28 | if model.pumps["rhr_p_2a"]["motor_breaker_closed"]:
29 | fluid.valves["rhr_v_64a"]["sealed_in"] = model.pumps["rhr_p_2a"]["actual_flow"] < 1200
30 |
31 | if model.pumps["rhr_p_2b"]["motor_breaker_closed"]:
32 | fluid.valves["rhr_v_64b"]["sealed_in"] = model.pumps["rhr_p_2b"]["actual_flow"] < 1200
33 |
34 | if model.pumps["rhr_p_2c"]["motor_breaker_closed"]:
35 | fluid.valves["rhr_v_64c"]["sealed_in"] = model.pumps["rhr_p_2c"]["actual_flow"] < 1200
36 |
37 | if reactor_inventory.rx_level_wr <= -129:
38 | if rhr_cb_init == False:
39 | rhr_cb_init = True
40 | rhr_cb_init_first = True
41 |
42 | model.alarms["rhr_bc_init_rpv_level_low"]["alarm"] = True
43 |
44 | if model.buttons["rhr_cb_init"]["state"]:
45 | rhr_cb_init = True
46 | rhr_cb_init_first = True
47 |
48 | if model.buttons["rhr_cb_init_reset"]["state"] and reactor_inventory.rx_level_wr > -129:
49 | rhr_cb_init = False
50 | rhr_cb_init_first = False
51 | model.alarms["rhr_bc_init_rpv_level_low"]["alarm"] = False
52 |
53 | if rhr_cb_init:
54 | if rhr_cb_init_first:
55 | model.pumps["rhr_p_2b"]["motor_breaker_closed"] = True
56 | model.pumps["rhr_p_2c"]["motor_breaker_closed"] = True
57 | rhr_cb_init_first = False
58 |
59 | fluid.valves["rhr_v_42b"]["sealed_in"] = True
60 | fluid.valves["rhr_v_42c"]["sealed_in"] = True
61 |
62 | if rhr_a_lpcs_init:
63 | if rhr_a_lpcs_init_first:
64 | model.pumps["rhr_p_2a"]["motor_breaker_closed"] = True
65 |
66 | fluid.valves["rhr_v_42a"]["sealed_in"] = True
67 | else:
68 | fluid.valves["rhr_v_42a"]["sealed_in"] = False
69 |
70 | model.alarms["rhr_bc_actuated"]["alarm"] = rhr_cb_init
71 | model.indicators["rhr_cb_init"] = rhr_cb_init
--------------------------------------------------------------------------------
/simulation/models/control_room_columbia/systems/rrc.py:
--------------------------------------------------------------------------------
1 | from simulation.models.control_room_columbia import model
2 | from simulation.models.control_room_columbia.general_physics import fluid
3 | from simulation.models.control_room_columbia.general_physics import ac_power
4 | from simulation.models.control_room_columbia.reactor_physics import reactor_inventory
5 | from simulation.models.control_room_columbia.systems import ESFAS
6 |
7 | asda = {
8 | "demand" : 15,
9 | "actual" : 0,
10 | "auto" : False,
11 | "started" : False,
12 |
13 | "master" : {
14 | "select" : True,
15 | "malfunction_high" : False,
16 | "malfunction_low" : False,
17 | },
18 | "slave" : {
19 | "select" : True,
20 | "malfunction_high" : False,
21 | "malfunction_low" : False,
22 | },
23 | }
24 |
25 | asdb = {
26 | "demand" : 15,
27 | "actual" : 0,
28 | "auto" : False,
29 | "started" : False,
30 |
31 | "master" : {
32 | "select" : True,
33 | "malfunction_high" : False,
34 | "malfunction_low" : False,
35 | },
36 | "slave" : {
37 | "select" : True,
38 | "malfunction_high" : False,
39 | "malfunction_low" : False,
40 | },
41 | }
42 |
43 | def runback_15hz():
44 | """runback all recirc pumps to min speed"""
45 | asda["auto"] = False
46 | asdb["auto"] = False
47 | asda["demand"] = 15
48 | asdb["demand"] = 15
49 |
50 | def trip_atws():
51 | """trip all recirculation pumps (atws)"""
52 | ac_power.breakers["cb_rpt_4a"].open()
53 | ac_power.breakers["cb_rpt_4b"].open()
54 |
55 |
56 | def start_permissives_a():
57 | if not asda["demand"] == 15:
58 | return False
59 |
60 | # if not asda["demand"] == 15: #TODO: Suction/discharge valves
61 | # return False
62 |
63 | # if not asda["demand"] == 15: #TODO: CB-RRB Lockout Relay Reset(?)
64 | # return False
65 |
66 | if asda["auto"]:
67 | return False
68 |
69 | return True
70 |
71 | def start_permissives_b():
72 | if not asdb["demand"] == 15:
73 | return False
74 |
75 | # if not asdb["demand"] == 15: #TODO: Suction/discharge valves
76 | # return False
77 |
78 | # if not asdb["demand"] == 15: #TODO: CB-RRB Lockout Relay Reset(?)
79 | # return False
80 |
81 | if asdb["auto"]:
82 | return False
83 |
84 | return True
85 |
86 | def run():
87 |
88 | if reactor_inventory.rx_level_wr <= ESFAS.RPV_LEVEL_3_in:
89 | #runback to 15hz
90 | model.alarms["rpv_level_low_limit"]["alarm"] = True
91 | runback_15hz()
92 | else:
93 | model.alarms["rpv_level_low_limit"]["alarm"] = False
94 |
95 | if reactor_inventory.rx_level_wr <= ESFAS.RPV_LEVEL_1_in:
96 | #trip on atws conditions
97 | model.alarms["recirc_a_pump_trip_atws_initiated"]["alarm"] = True
98 | model.alarms["recirc_b_pump_trip_atws_initiated"]["alarm"] = True
99 | trip_atws()
100 | else:
101 | model.alarms["recirc_a_pump_trip_atws_initiated"]["alarm"] = False
102 | model.alarms["recirc_b_pump_trip_atws_initiated"]["alarm"] = False
103 |
104 | #Should we actually properly simulate the ASD?
105 |
106 | if ac_power.busses["6"].is_voltage_at_bus(4160) and asdb["started"]:
107 | #TODO: Actual effect of different channel selection
108 | if asdb["master"]["select"] and not asdb["slave"]["select"]:
109 | asdb["actual"] += min(asdb["demand"]-asdb["actual"],0.6)
110 |
111 | elif not asdb["master"]["select"] and asdb["slave"]["select"]:
112 | asdb["actual"] += min(asdb["demand"]-asdb["actual"],0.6)
113 |
114 | elif asdb["master"]["select"] and asdb["slave"]["select"]:
115 | asdb["actual"] += min(asdb["demand"]-asdb["actual"],0.6)
116 |
117 | else:#Would this actually happen?
118 | asdb["actual"] = 0
119 | asdb["started"] = False
120 |
121 | else:
122 | asdb["actual"] = 0
123 | asdb["started"] = False
124 |
125 | if ac_power.busses["5"].is_voltage_at_bus(4160) and asda["started"]:
126 | #TODO: Actual effect of different channel selection
127 | if asda["master"]["select"] and not asda["slave"]["select"]:
128 | asda["actual"] += min(asda["demand"]-asda["actual"],0.6)
129 |
130 | elif not asda["master"]["select"] and asda["slave"]["select"]:
131 | asda["actual"] += min(asda["demand"]-asda["actual"],0.6)
132 |
133 | elif asda["master"]["select"] and asda["slave"]["select"]:
134 | asda["actual"] += min(asda["demand"]-asda["actual"],0.6)
135 |
136 | else:#Would this actually happen?
137 | asda["actual"] = 0
138 | asda["started"] = False
139 |
140 | else:
141 | asda["actual"] = 0
142 | asda["started"] = False
143 |
144 |
145 | if model.buttons["rrc_b_start"]["state"] and start_permissives_b():
146 | asdb["started"] = True
147 |
148 | if model.buttons["rrc_a_start"]["state"] and start_permissives_a():
149 | asda["started"] = True
150 |
151 | if model.buttons["station_1b_lower"]["state"]:
152 | asdb["demand"] = max(15,asdb["demand"]-0.1)
153 |
154 | if model.buttons["station_1b_raise"]["state"]:
155 | asdb["demand"] = min(65,asdb["demand"]+0.1)
156 |
157 | if model.buttons["station_1a_lower"]["state"]:
158 | asda["demand"] = max(15,asda["demand"]-0.1)
159 |
160 | if model.buttons["station_1a_raise"]["state"]:
161 | asda["demand"] = min(65,asda["demand"]+0.1)
162 |
163 |
164 |
165 | model.values["rrc_p_1b_volts"] = ac_power.busses["asdb"].voltage_at_bus()
166 | model.values["rrc_p_1b_amps"] = model.pumps["rrc_p_1b"]["amperes"]
167 | model.values["rrc_p_1b_freq"] = asdb["actual"]
168 | model.values["rrc_p_1b_speed"] = model.pumps["rrc_p_1b"]["rpm"]
169 |
170 | model.values["station_1b_flow"] = model.pumps["rrc_p_1b"]["flow"]
171 | model.values["station_1b_bias"] = asdb["actual"]-asdb["demand"]
172 | model.values["station_1b_demand"] = asdb["demand"]
173 | model.values["station_1b_actual"] = asdb["actual"]
174 |
175 | ac_power.sources["ASDB"].info["frequency"] = asdb["actual"]
176 | ac_power.sources["ASDB"].info["voltage"] = ac_power.busses["6"].voltage_at_bus()#asdb["actual"]*115 #115 volts per hertz
177 |
178 | model.values["rrc_p_1a_volts"] = ac_power.busses["asda"].voltage_at_bus()
179 | model.values["rrc_p_1a_amps"] = model.pumps["rrc_p_1a"]["amperes"]
180 | model.values["rrc_p_1a_freq"] = asda["actual"]
181 | model.values["rrc_p_1a_speed"] = model.pumps["rrc_p_1a"]["rpm"]
182 |
183 | model.values["station_1a_flow"] = model.pumps["rrc_p_1a"]["flow"]
184 | model.values["station_1a_bias"] = asda["actual"]-asda["demand"]
185 | model.values["station_1a_demand"] = asda["demand"]
186 | model.values["station_1a_actual"] = asda["actual"]
187 |
188 | ac_power.sources["ASDA"].info["frequency"] = asda["actual"]
189 | ac_power.sources["ASDA"].info["voltage"] = ac_power.busses["5"].voltage_at_bus()#asda["actual"]*115 #115 volts per hertz
--------------------------------------------------------------------------------
/simulation/models/control_room_columbia/systems/safety_relief.py:
--------------------------------------------------------------------------------
1 | from simulation.models.control_room_columbia import model
2 | from simulation.models.control_room_columbia.reactor_physics import reactor_inventory
3 | from simulation.models.control_room_columbia.reactor_physics import pressure
4 | from simulation.models.control_room_columbia.systems import ESFAS
5 |
6 | safety_relief = {
7 | "ms_rv_5b" : {
8 | "valve" : 1,
9 | "open_percent" : 0,
10 | "auto" : 1131,
11 | "safety_auto" : 1205,
12 | "flow" : 906250,
13 | "ads" : True,
14 | "malf_closed" : False,
15 | "malf_open" : False,
16 | },
17 | "ms_rv_3d" : {
18 | "valve" : 2,
19 | "open_percent" : 0,
20 | "auto" : 1121,
21 | "safety_auto" : 1195,
22 | "flow" : 898800,
23 | "ads" : True,
24 | "malf_closed" : False,
25 | "malf_open" : False,
26 | },
27 | "ms_rv_5c" : {
28 | "valve" : 3,
29 | "open_percent" : 0,
30 | "auto" : 1131,
31 | "safety_auto" : 1205,
32 | "flow" : 906250,
33 | "ads" : True,
34 | "malf_closed" : False,
35 | "malf_open" : False,
36 | },
37 | "ms_rv_4d" : {
38 | "valve" : 4,
39 | "open_percent" : 0,
40 | "auto" : 1131,
41 | "safety_auto" : 1205,
42 | "flow" : 906250,
43 | "ads" : True,
44 | "malf_closed" : False,
45 | "malf_open" : False,
46 | },
47 | "ms_rv_4b" : {
48 | "valve" : 5,
49 | "open_percent" : 0,
50 | "auto" : 1121,
51 | "safety_auto" : 1195,
52 | "flow" : 898800,
53 | "ads" : True,
54 | "malf_closed" : False,
55 | "malf_open" : False,
56 | },
57 | "ms_rv_4a" : {
58 | "valve" : 6,
59 | "open_percent" : 0,
60 | "auto" : 1131,
61 | "safety_auto" : 1205,
62 | "flow" : 906250,
63 | "ads" : True,
64 | "malf_closed" : False,
65 | "malf_open" : False,
66 | },
67 | "ms_rv_4c" : {
68 | "valve" : 7,
69 | "open_percent" : 0,
70 | "auto" : 1121,
71 | "safety_auto" : 1195,
72 | "flow" : 898800,
73 | "ads" : True,
74 | "malf_closed" : False,
75 | "malf_open" : False,
76 | },
77 | "ms_rv_1a" : {
78 | "valve" : 8,
79 | "open_percent" : 0,
80 | "auto" : 1101,
81 | "flow" : 883950,
82 | "safety_auto" : 1175,
83 | "ads" : False,
84 | "malf_closed" : False,
85 | "malf_open" : False,
86 | },
87 | "ms_rv_2b" : {
88 | "valve" : 9,
89 | "open_percent" : 0,
90 | "auto" : 1101,
91 | "flow" : 883950,
92 | "safety_auto" : 1175,
93 | "ads" : False,
94 | "malf_closed" : False,
95 | "malf_open" : False,
96 | },
97 | "ms_rv_1c" : {
98 | "valve" : 10,
99 | "open_percent" : 0,
100 | "auto" : 1091,
101 | "safety_auto" : 1165,
102 | "flow" : 876118,
103 | "ads" : False,
104 | "malf_closed" : False,
105 | "malf_open" : False,
106 | },
107 | "ms_rv_1b" : {
108 | "valve" : 11,
109 | "open_percent" : 0,
110 | "auto" : 1091,
111 | "flow" : 876118,
112 | "safety_auto" : 1165,
113 | "ads" : False,
114 | "malf_closed" : False,
115 | "malf_open" : False,
116 | },
117 | "ms_rv_2c" : {
118 | "valve" : 12,
119 | "open_percent" : 0,
120 | "auto" : 1101,
121 | "flow" : 883950,
122 | "safety_auto" : 1175,
123 | "ads" : False,
124 | "malf_closed" : False,
125 | "malf_open" : False,
126 | },
127 | "ms_rv_1d" : {
128 | "valve" : 13,
129 | "open_percent" : 0,
130 | "auto" : 1101,
131 | "flow" : 883950,
132 | "safety_auto" : 1175,
133 | "ads" : False,
134 | "malf_closed" : False,
135 | "malf_open" : False,
136 | },
137 | "ms_rv_3c" : {
138 | "valve" : 14,
139 | "open_percent" : 0,
140 | "auto" : 1111,
141 | "safety_auto" : 1185,
142 | "flow" : 891380,
143 | "ads" : False,
144 | "malf_closed" : False,
145 | "malf_open" : False,
146 | },
147 | "ms_rv_2d" : {
148 | "valve" : 15,
149 | "open_percent" : 0,
150 | "auto" : 1111,
151 | "safety_auto" : 1185,
152 | "flow" : 891380,
153 | "ads" : False,
154 | "malf_closed" : False,
155 | "malf_open" : False,
156 | },
157 | "ms_rv_2a" : {
158 | "valve" : 16,
159 | "open_percent" : 0,
160 | "auto" : 1111,
161 | "safety_auto" : 1185,
162 | "flow" : 891380,
163 | "ads" : False,
164 | "malf_closed" : False,
165 | "malf_open" : False,
166 | },
167 | "ms_rv_3b" : {
168 | "valve" : 17,
169 | "open_percent" : 0,
170 | "auto" : 1111,
171 | "safety_auto" : 1185,
172 | "flow" : 891380,
173 | "ads" : False,
174 | "malf_closed" : False,
175 | "malf_open" : False,
176 | },
177 | "ms_rv_3a" : {
178 | "valve" : 18,
179 | "open_percent" : 0,
180 | "auto" : 1121,
181 | "safety_auto" : 1195,
182 | "flow" : 898800,
183 | "ads" : False,
184 | "malf_closed" : False,
185 | "malf_open" : False,
186 | },
187 | }
188 |
189 | def run():
190 | model.alarms["srv_open"]["alarm"] = False
191 |
192 | global safety_relief
193 |
194 | for valve_name in safety_relief:
195 | valve = safety_relief[valve_name]
196 | operator_off = False
197 | operator_open = False
198 | relief_open = False
199 | safety_open = False
200 | if valve["ads"] == True :
201 | ads_open = (ESFAS.ADS_1.ADS_SYS_INITIATED or ESFAS.ADS_2.ADS_SYS_INITIATED)
202 | else:
203 | ads_open = False
204 |
205 | relief_open = pressure.Pressures["Vessel"]/6895 >= valve["auto"]
206 | relief_close = pressure.Pressures["Vessel"]/6895 <= valve["auto"] - 40 #closes 40 psig below the setpoint
207 | safety_open = pressure.Pressures["Vessel"]/6895 >= valve["safety_auto"]
208 |
209 | if valve_name in model.switches:
210 | control_sw = model.switches[valve_name]
211 | if control_sw["position"] == 0:
212 | operator_off = True
213 | elif control_sw["position"] == 2:
214 | operator_open = True
215 |
216 | control_sw["lights"]["red"] = bool(operator_open or relief_open)
217 | control_sw["lights"]["green"] = bool(not operator_open and not relief_open) #light does not indicate if the valve is actually open (didnt we learn from TMI???)
218 |
219 | if ((operator_open or ads_open) and not operator_off) or (safety_open):
220 | valve["open_percent"] = max(min(valve["open_percent"]+10,100),0)
221 | elif (relief_open and not operator_off):
222 | valve["open_percent"] = max(min(valve["open_percent"]+50,100),0)
223 | elif relief_close or operator_off:
224 | valve["open_percent"] = max(min(valve["open_percent"]-50,100),0)
225 |
226 | if valve["open_percent"] > 0:
227 | model.alarms["srv_open"]["alarm"] = True
228 |
229 | #take pounds per hour
230 | #becomes kilograms per hour (0.4536)
231 | #to kilograms per second 1/(60*60) = 1/3600
232 |
233 | SRVOutflow = ((valve["flow"]*0.4536)/(60*60))*((pressure.Pressures["Vessel"]/(valve["auto"]*6895)))*(valve["open_percent"]/100)
234 |
235 | SRVOutflow = SRVOutflow*0.1 #sim time
236 |
237 | if SRVOutflow > 0:
238 | reactor_inventory.remove_steam(SRVOutflow)
--------------------------------------------------------------------------------
/simulation/models/control_room_columbia/systems/service_water.py:
--------------------------------------------------------------------------------
1 | from simulation.models.control_room_columbia import model
2 | from simulation.models.control_room_columbia.general_physics import fluid
3 |
4 | def run():
5 | if fluid.headers["sw_p_1a_discharge"]["pressure"]/6895 > 50:
6 | fluid.valves["sw_v_2a"]["sealed_in"] = True
7 |
8 | model.values["sw_a_flow"] = fluid.valves["rhr_v_68a"]["flow"]*15.85032
9 | model.values["sw_a_press"] = fluid.headers["sw_a_return"]["pressure"]/6895
10 | model.values["sw_p_1a_amps"] = model.pumps["sw_p_1a"]["amperes"]
11 | model.values["sw_a_temp"] = (fluid.SWPondATemp*1.8) + 32
12 | #print(fluid.SWPondATemp, model.reactor_water_temperature)
13 |
--------------------------------------------------------------------------------
/simulation/models/control_room_columbia/systems/sync.py:
--------------------------------------------------------------------------------
1 | from simulation.models.control_room_columbia.general_physics import ac_power
2 | from simulation.models.control_room_columbia import model
3 |
4 | def get_phase(name):
5 | if name in ac_power.sources:
6 | return ac_power.sources[name].info["phase"]
7 |
8 | return ac_power.busses[name].info["phase"]
9 |
10 | def get_voltage(name):
11 | if name in ac_power.sources:
12 | return ac_power.sources[name].info["voltage"]
13 |
14 | return ac_power.busses[name].info["voltage"]
15 |
16 | class SyncSelector():
17 |
18 | def __init__(self,sync_name):
19 | self.selectors = {}
20 | self.synchroscope = sync_name
21 | self.incoming = None
22 | self.running = None
23 |
24 | def add_voltage_gauges(self,incoming,running):
25 | self.incoming=incoming
26 | self.running=running
27 |
28 | def add_selector(self,name,incoming,running,breaker):
29 |
30 | self.selectors[name] = {"incoming" : incoming, "running" : running, "breaker" : breaker}
31 |
32 | ac_power.breakers[breaker].info["sync_sel"] = True
33 |
34 | def check_selectors(self):
35 |
36 | active_selector = ""
37 |
38 | for selector in self.selectors:
39 |
40 | if model.switches[selector]["position"] != 1:
41 | active_selector = selector
42 | break
43 |
44 | if active_selector != "":
45 | sel = self.selectors[active_selector]
46 | model.values[self.synchroscope] = get_phase(sel["running"])-get_phase(sel["incoming"])
47 |
48 | if model.switches[active_selector]["position"] == 0:
49 | ac_power.breakers[sel["breaker"]].info["sync"] = True
50 |
51 | if self.incoming != None and self.running != None:
52 | model.values[self.incoming] = get_voltage(sel["incoming"])
53 | model.values[self.running] = get_voltage(sel["running"])
54 | else:
55 |
56 | if self.incoming != None and self.running != None:
57 | model.values[self.incoming] = 0
58 | model.values[self.running] = 0
59 |
60 |
61 | synchroscopes = []
62 |
63 | def initialize():
64 | sync = SyncSelector("main_generator_sync")
65 | sync.add_selector("sync_cb_4885","main_bus","ASHE500","cb_4885")
66 | sync.add_selector("sync_cb_4888","main_bus","ASHE500","cb_4888")
67 | synchroscopes.append(sync)
68 |
69 | sync = SyncSelector("div_1_sync")
70 | sync.add_voltage_gauges("sm7incoming","sm7running")
71 | sync.add_selector("sync_cb_dg1_7","DG1","7","cb_dg1_7")
72 | sync.add_selector("sync_cb_b7","b_bus","7","cb_b7")
73 | sync.add_selector("sync_cb_7_1","1","7","cb_1_7")
74 | synchroscopes.append(sync)
75 |
76 | sync = SyncSelector("div_2_sync")
77 | sync.add_voltage_gauges("sm8incoming","sm8running")
78 | sync.add_selector("sync_cb_dg2_8","DG2","8","cb_dg2_8")
79 | sync.add_selector("sync_cb_b8","b_bus","8","cb_b8")
80 | sync.add_selector("sync_cb_8_3","3","8","cb_3_8")
81 | synchroscopes.append(sync)
82 |
83 | def run():
84 | for synchroscope in synchroscopes:
85 | synchroscope.check_selectors()
--------------------------------------------------------------------------------
/simulation/models/dev_test/model.py:
--------------------------------------------------------------------------------
1 | from simulation.constants.annunciator_states import AnnunciatorStates
2 | import math
3 |
4 | alarms = {
5 | "test_alarm" : {
6 | "alarm" : False,
7 | "box": "Box1",
8 | "window": "1-2",
9 | "state" : AnnunciatorStates.CLEAR,
10 | "group" : "1",
11 | "silenced" : False,
12 | },
13 | }
14 |
15 | switches = {
16 | "test_switch": {
17 | "positions": {
18 | 0: 45,
19 | 1: 0,
20 | 2: -45,
21 | },
22 | "position": 0,
23 | "lights" : {
24 | "green" : True,
25 | "red" : False,
26 | },
27 | "flag" : "green",
28 | "momentary" : False,
29 | },
30 | "test_switch2": {
31 | "positions": {
32 | 0: 45,
33 | 1: 0,
34 | 2: -45,
35 | },
36 | "position": 0,
37 | "lights" : {
38 | "green" : True,
39 | "red" : False,
40 | },
41 | "flag" : "green",
42 | "momentary" : False,
43 | },
44 | }
45 |
46 | values = {
47 | "test_gauge": 0.1
48 | }
49 |
50 | indicators = {
51 | "lamp_test": True
52 | }
53 |
54 | buttons = {
55 | "test_button": False
56 | }
57 |
58 | recorders = {}
59 |
60 | test_value = 0
61 |
62 | def model_run(delta):
63 | global test_value
64 | if alarms == {}:
65 | return
66 | if buttons["test_button"]:
67 | test_value += 0.1
68 | values["test_gauge"] = math.sin(test_value) / 2 + 0.5
69 | indicators["lamp_test"] = switches["test_switch"]["position"] != 1
70 | alarms["test_alarm"]["state"] = AnnunciatorStates.CLEAR
71 | if switches["test_switch"]["position"] == 0:
72 | alarms["test_alarm"]["state"] = AnnunciatorStates.ACTIVE
73 | elif switches["test_switch"]["position"] == 2:
74 | alarms["test_alarm"]["state"] = AnnunciatorStates.ACTIVE_CLEAR
75 |
76 | def model_run_fast(delta):
77 | pass
78 |
--------------------------------------------------------------------------------