├── .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 | --------------------------------------------------------------------------------