├── simglucose ├── actuator │ ├── __init__.py │ └── pump.py ├── analysis │ ├── __init__.py │ ├── analysis.log │ ├── risk.py │ └── report.py ├── controller │ ├── __init__.py │ ├── base.py │ ├── pid_ctrller.py │ └── basal_bolus_ctrller.py ├── patient │ ├── __init__.py │ ├── base.py │ └── t1dpatient.py ├── sensor │ ├── __init__.py │ ├── cgm.py │ └── noise_gen.py ├── simulation │ ├── __init__.py │ ├── scenario.py │ ├── sim_engine.py │ ├── scenario_gen.py │ ├── rendering.py │ ├── env.py │ └── user_interface.py ├── envs │ ├── __init__.py │ └── simglucose_gym_env.py ├── __init__.py ├── params │ ├── pump_params.csv │ ├── sensor_params.csv │ ├── Quest.csv │ └── vpatient_params.csv └── utils.py ├── screenshots ├── CVGA.png ├── animate.png ├── risk_index.png ├── zone_stats.png └── BG_trace_plot.png ├── examples ├── run_user_interface.py ├── results │ └── 2017-12-31_17-46-32 │ │ ├── CVGA.png │ │ ├── BG_trace.png │ │ ├── CVGA_stats.csv │ │ ├── risk_stats.png │ │ ├── zone_stats.png │ │ ├── performance_stats.csv │ │ └── risk_trace.csv ├── run_pid_controller.py ├── offline_analysis.py ├── run_gymnasium.py ├── custom_reward_function.py ├── run_gym.py ├── run_rllab.py ├── run_multi_patient_multi_scenario.py ├── apply_customized_controller.py └── advanced_tutorial.py ├── pip_release.sh ├── .gitignore ├── MANIFEST.in ├── requirements.txt ├── setup.py ├── tests ├── test_render.py ├── test_seed.py ├── test_ui.py ├── test_gymnasium.py ├── test_pid_controller.py ├── test_gym.py ├── test_report.py ├── test_rllab.py ├── test_reward_fun.py ├── test_gym_custom_scenario.py ├── test_reset.py └── test_sim_engine.py ├── .github └── workflows │ ├── python-publish.yml │ └── python-package.yml ├── LICENSE ├── definitions_of_vpatient_parameters.md └── README.md /simglucose/actuator/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /simglucose/analysis/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /simglucose/analysis/analysis.log: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /simglucose/controller/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /simglucose/patient/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /simglucose/sensor/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /simglucose/simulation/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /screenshots/CVGA.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jxx123/simglucose/HEAD/screenshots/CVGA.png -------------------------------------------------------------------------------- /screenshots/animate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jxx123/simglucose/HEAD/screenshots/animate.png -------------------------------------------------------------------------------- /examples/run_user_interface.py: -------------------------------------------------------------------------------- 1 | from simglucose.simulation.user_interface import simulate 2 | simulate() 3 | -------------------------------------------------------------------------------- /screenshots/risk_index.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jxx123/simglucose/HEAD/screenshots/risk_index.png -------------------------------------------------------------------------------- /screenshots/zone_stats.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jxx123/simglucose/HEAD/screenshots/zone_stats.png -------------------------------------------------------------------------------- /screenshots/BG_trace_plot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jxx123/simglucose/HEAD/screenshots/BG_trace_plot.png -------------------------------------------------------------------------------- /examples/results/2017-12-31_17-46-32/CVGA.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jxx123/simglucose/HEAD/examples/results/2017-12-31_17-46-32/CVGA.png -------------------------------------------------------------------------------- /pip_release.sh: -------------------------------------------------------------------------------- 1 | rm -r build dist simglucose.egg-info 2 | python setup.py sdist bdist_wheel 3 | # python -m build 4 | twine upload dist/* -r jxx123 -------------------------------------------------------------------------------- /examples/results/2017-12-31_17-46-32/BG_trace.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jxx123/simglucose/HEAD/examples/results/2017-12-31_17-46-32/BG_trace.png -------------------------------------------------------------------------------- /examples/results/2017-12-31_17-46-32/CVGA_stats.csv: -------------------------------------------------------------------------------- 1 | ,A,B,C,D,E 2 | 0,0.0,0.16666666666666666,0.03333333333333333,0.4666666666666667,0.3333333333333333 3 | -------------------------------------------------------------------------------- /examples/results/2017-12-31_17-46-32/risk_stats.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jxx123/simglucose/HEAD/examples/results/2017-12-31_17-46-32/risk_stats.png -------------------------------------------------------------------------------- /examples/results/2017-12-31_17-46-32/zone_stats.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jxx123/simglucose/HEAD/examples/results/2017-12-31_17-46-32/zone_stats.png -------------------------------------------------------------------------------- /simglucose/envs/__init__.py: -------------------------------------------------------------------------------- 1 | from simglucose.envs.simglucose_gym_env import T1DSimEnv 2 | from simglucose.envs.simglucose_gym_env import T1DSimGymnaisumEnv 3 | -------------------------------------------------------------------------------- /simglucose/__init__.py: -------------------------------------------------------------------------------- 1 | from gym.envs.registration import register 2 | 3 | register( 4 | id='simglucose-v0', 5 | entry_point='simglucose.envs:T1DSimEnv', 6 | ) 7 | -------------------------------------------------------------------------------- /simglucose/params/pump_params.csv: -------------------------------------------------------------------------------- 1 | Name,min_bolus,max_bolus,inc_bolus,min_basal,max_basal,inc_basal,sample_time 2 | Cozmo,0.0,75.0,0.05,0.0,35.0,0.05,1.0 3 | Insulet,0.0,30.0,0.05,0.0,30.0,0.05,1.0 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled python modules. 2 | *.pyc 3 | 4 | # results foler 5 | /results/ 6 | 7 | # Setuptools distribution folder. 8 | /dist/ 9 | /build/ 10 | 11 | # Python egg metadata, regenerated from source files by setuptools. 12 | /*.egg-info 13 | /*.egg 14 | -------------------------------------------------------------------------------- /simglucose/params/sensor_params.csv: -------------------------------------------------------------------------------- 1 | Name,PACF,gamma,lambda,delta,xi,sample_time,min,max 2 | Dexcom,0.7,-0.5444,15.9574,1.6898,-5.47,3.0,39.0,600.0 3 | GuardianRT,0.7,-0.5444,15.9574,1.6898,-5.47,5.0,39.0,600.0 4 | Navigator,0.7,-0.5444,15.9574,1.6898,-5.47,1.0,32.0,600.0 5 | -------------------------------------------------------------------------------- /examples/run_pid_controller.py: -------------------------------------------------------------------------------- 1 | from simglucose.controller.pid_ctrller import PIDController 2 | from simglucose.simulation.user_interface import simulate 3 | 4 | 5 | pid_controller = PIDController(P=0.001, I=0.00001, D=0.001, target=140) 6 | s = simulate(controller=pid_controller) 7 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include simglucose/params/*.csv 2 | include LICENSE 3 | include *.py 4 | include examples/*.py 5 | include tests/*.py 6 | include tests/*.csv 7 | recursive-include simglucose *.py 8 | include README.md 9 | include tests/*.csv 10 | include examples/results/2017-12-31_17-46-32/*.csv -------------------------------------------------------------------------------- /examples/offline_analysis.py: -------------------------------------------------------------------------------- 1 | from simglucose.analysis.report import report 2 | import pandas as pd 3 | from pathlib import Path 4 | 5 | 6 | # get the path to the example folder 7 | exmaple_pth = Path(__file__).parent 8 | 9 | # find all csv with pattern *#*.csv, e.g. adolescent#001.csv 10 | result_filenames = list(exmaple_pth.glob( 11 | 'results/2017-12-31_17-46-32/*#*.csv')) 12 | patient_names = [f.stem for f in result_filenames] 13 | df = pd.concat( 14 | [pd.read_csv(str(f), index_col=0) for f in result_filenames], 15 | keys=patient_names) 16 | report(df) 17 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | certifi>=2023.7.22 2 | charset-normalizer>=3.2.0 3 | cloudpickle>=2.2.1 4 | contourpy>=1.1.0 5 | cycler>=0.11.0 6 | dill>=0.3.7 7 | Farama-Notifications>=0.0.4 8 | fonttools>=4.42.0 9 | gym==0.9.4 10 | gymnasium~=0.29.1 11 | idna>=3.4 12 | iniconfig>=2.0.0 13 | kiwisolver>=1.4.4 14 | matplotlib>=3.7.2 15 | multiprocess>=0.70.15 16 | numpy>=1.25.0 17 | packaging>=23.1 18 | pandas>=2.0.3 19 | pathos>=0.3.1 20 | Pillow>=10.0.0 21 | pluggy>=1.2.0 22 | pox>=0.3.3 23 | ppft>=1.7.6.7 24 | pyglet>=2.0.9 25 | pyparsing>=3.0.9 26 | pytest>=7.4.0 27 | python-dateutil>=2.8.2 28 | pytz>=2023.3 29 | requests>=2.31.0 30 | scipy>=1.11.0 31 | six>=1.16.0 32 | typing_extensions>=4.7.1 33 | tzdata>=2023.3 34 | urllib3>=2.0.4 35 | -------------------------------------------------------------------------------- /simglucose/patient/base.py: -------------------------------------------------------------------------------- 1 | """Base class for patient""" 2 | 3 | 4 | class Patient(object): 5 | def step(self, action): 6 | """ 7 | Run one time step of the patient dynamics 8 | ------ 9 | Input 10 | action: a namedtuple 11 | ------ 12 | Outputs 13 | t: current time 14 | state: updated state 15 | observation: the observable states 16 | """ 17 | raise NotImplementedError 18 | 19 | @staticmethod 20 | def model(t, state, action, params): 21 | """ 22 | ordinary differential equations 23 | """ 24 | raise NotImplementedError 25 | 26 | def reset(self): 27 | """ 28 | Reset to the initial state 29 | Return observation 30 | """ 31 | raise NotImplementedError 32 | -------------------------------------------------------------------------------- /examples/run_gymnasium.py: -------------------------------------------------------------------------------- 1 | import gymnasium 2 | from gymnasium.envs.registration import register 3 | 4 | register( 5 | id="simglucose/adolescent2-v0", 6 | entry_point="simglucose.envs:T1DSimGymnaisumEnv", 7 | max_episode_steps=10, 8 | kwargs={"patient_name": "adolescent#002"}, 9 | ) 10 | 11 | env = gymnasium.make("simglucose/adolescent2-v0", render_mode="human") 12 | observation, info = env.reset() 13 | for t in range(200): 14 | env.render() 15 | action = env.action_space.sample() 16 | observation, reward, terminated, truncated, info = env.step(action) 17 | print( 18 | f"Step {t}: observation {observation}, reward {reward}, terminated {terminated}, truncated {truncated}, info {info}" 19 | ) 20 | if terminated or truncated: 21 | print("Episode finished after {} timesteps".format(t + 1)) 22 | break 23 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup( 4 | name="simglucose", 5 | version="0.2.11", 6 | description="A Type-1 Diabetes Simulator as a Reinforcement Learning Environment in OpenAI gym or rllab (python implementation of UVa/Padova Simulator)", 7 | url="https://github.com/jxx123/simglucose", 8 | author="Jinyu Xie", 9 | author_email="xjygr08@gmail.com", 10 | license="MIT", 11 | packages=["simglucose"], 12 | install_requires=[ 13 | "gym==0.9.4", 14 | "gymnasium~=0.29.1", 15 | "pathos>=0.3.1", 16 | "scipy>=1.11.0", 17 | "matplotlib>=3.7.2", 18 | "numpy>=1.25.0", 19 | "pandas>=2.0.3", 20 | ], 21 | include_package_data=True, 22 | zip_safe=False, 23 | long_description=open("README.md").read(), 24 | long_description_content_type="text/markdown", 25 | ) 26 | -------------------------------------------------------------------------------- /tests/test_render.py: -------------------------------------------------------------------------------- 1 | from simglucose.simulation.rendering import Viewer 2 | from datetime import datetime 3 | import pandas as pd 4 | import unittest 5 | import logging 6 | import os 7 | 8 | logger = logging.getLogger(__name__) 9 | TESTDATA_FILENAME = os.path.join(os.path.dirname(__file__), 'sim_results.csv') 10 | 11 | 12 | class TestRendering(unittest.TestCase): 13 | def setUp(self): 14 | self.df = pd.read_csv(TESTDATA_FILENAME, index_col=0) 15 | self.df.index = pd.to_datetime(self.df.index) 16 | 17 | def test_rendering(self): 18 | start_time = datetime(2018, 1, 1, 0, 0, 0) 19 | viewer = Viewer(start_time, 'adolescent#001') 20 | for i in range(len(self.df)): 21 | df_tmp = self.df.iloc[0:(i + 1), :] 22 | viewer.render(df_tmp) 23 | viewer.close() 24 | 25 | 26 | if __name__ == '__main__': 27 | unittest.main() 28 | -------------------------------------------------------------------------------- /tests/test_seed.py: -------------------------------------------------------------------------------- 1 | import gym 2 | import unittest 3 | from datetime import datetime 4 | from gym.envs.registration import register 5 | 6 | register( 7 | id='simglucose-adult1-v0', 8 | entry_point='simglucose.envs:T1DSimEnv', 9 | kwargs={'patient_name': 'adult#001'} 10 | ) 11 | 12 | 13 | class TestSeed(unittest.TestCase): 14 | def test_changing_seed_generates_different_results(self): 15 | env = gym.make('simglucose-adult1-v0') 16 | 17 | env.seed(0) 18 | observation_seed0 = env.reset() 19 | self.assertEqual(env.env.scenario.start_time, datetime(2018, 1, 1, 23, 0, 0)) 20 | 21 | env.seed(1000) 22 | observation_seed1 = env.reset() 23 | self.assertEqual(env.env.scenario.start_time, datetime(2018, 1, 1, 14, 0, 0)) 24 | 25 | self.assertNotEqual(observation_seed0, observation_seed1) 26 | 27 | 28 | if __name__ == '__main__': 29 | unittest.main() 30 | -------------------------------------------------------------------------------- /simglucose/utils.py: -------------------------------------------------------------------------------- 1 | import pkg_resources 2 | import pandas as pd 3 | 4 | CONTROL_QUEST = pkg_resources.resource_filename('simglucose', 5 | 'params/Quest.csv') 6 | PATIENT_PARA_FILE = pkg_resources.resource_filename( 7 | 'simglucose', 'params/vpatient_params.csv') 8 | 9 | 10 | def fetch_patient_params(patient_name: str): 11 | all_params = pd.read_csv(PATIENT_PARA_FILE) 12 | patient_params = lookup_patient_meta_data(all_params, patient_name) 13 | return patient_params 14 | 15 | 16 | def fetch_patient_quest(patient_name: str): 17 | all_quests = pd.read_csv(CONTROL_QUEST) 18 | quest = lookup_patient_meta_data(all_quests, patient_name) 19 | return quest 20 | 21 | 22 | def lookup_patient_meta_data(df: pd.DataFrame, patient_name: str) -> dict: 23 | idx = df['Name'] == patient_name 24 | params = {} 25 | if idx.any(): 26 | params = df[idx].iloc[0].to_dict() 27 | return params 28 | -------------------------------------------------------------------------------- /examples/custom_reward_function.py: -------------------------------------------------------------------------------- 1 | import gym 2 | from gym.envs.registration import register 3 | 4 | 5 | def custom_reward(BG_last_hour): 6 | if BG_last_hour[-1] > 180: 7 | return -1 8 | elif BG_last_hour[-1] < 70: 9 | return -2 10 | else: 11 | return 1 12 | 13 | 14 | register( 15 | id='simglucose-adolescent2-v0', 16 | entry_point='simglucose.envs:T1DSimEnv', 17 | kwargs={'patient_name': 'adolescent#002', 18 | 'reward_fun': custom_reward} 19 | ) 20 | 21 | env = gym.make('simglucose-adolescent2-v0') 22 | 23 | reward = 1 24 | done = False 25 | 26 | observation = env.reset() 27 | for t in range(200): 28 | env.render(mode='human') 29 | action = env.action_space.sample() 30 | observation, reward, done, info = env.step(action) 31 | print(observation) 32 | print("Reward = {}".format(reward)) 33 | if done: 34 | print("Episode finished after {} timesteps".format(t + 1)) 35 | break 36 | -------------------------------------------------------------------------------- /tests/test_ui.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from unittest.mock import patch 3 | from simglucose.simulation.user_interface import simulate 4 | import shutil 5 | import os 6 | import pandas as pd 7 | 8 | output_folder = os.path.join(os.path.dirname(__file__), 'results') 9 | 10 | 11 | class testUI(unittest.TestCase): 12 | def setUp(self): 13 | pass 14 | 15 | @patch('builtins.input') 16 | def test_ui(self, mock_input): 17 | # animation, parallel, save_path, sim_time, scenario, scenario random 18 | # seed, start_time, patients, sensor, sensor seed, insulin pump, 19 | # controller 20 | mock_input.side_effect = [ 21 | 'n', 'y', output_folder, '24', '1', '2', '6', '1', '1', '1', '2', 22 | '1' 23 | ] 24 | results = simulate() 25 | self.assertIsInstance(results, pd.DataFrame) 26 | 27 | def tearDown(self): 28 | shutil.rmtree(output_folder) 29 | 30 | 31 | if __name__ == '__main__': 32 | unittest.main() 33 | -------------------------------------------------------------------------------- /.github/workflows/python-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflows will upload a Python Package using Twine when a release is created 2 | # For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries 3 | 4 | name: Upload Python Package 5 | 6 | on: 7 | release: 8 | types: [created] 9 | 10 | jobs: 11 | deploy: 12 | 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v2 17 | - name: Set up Python 18 | uses: actions/setup-python@v2 19 | with: 20 | python-version: '3.x' 21 | - name: Install dependencies 22 | run: | 23 | python -m pip install --upgrade pip 24 | pip install setuptools wheel twine 25 | - name: Build and publish 26 | env: 27 | TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} 28 | TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} 29 | run: | 30 | python setup.py sdist bdist_wheel 31 | twine upload dist/* 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 Jinyu Xie 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. -------------------------------------------------------------------------------- /tests/test_gymnasium.py: -------------------------------------------------------------------------------- 1 | import gymnasium 2 | import unittest 3 | from gymnasium.envs.registration import register 4 | 5 | 6 | class TestGymnasium(unittest.TestCase): 7 | def test_gymnasium_random_agent(self): 8 | register( 9 | id="simglucose/adolescent2-v0", 10 | entry_point="simglucose.envs:T1DSimGymnaisumEnv", 11 | max_episode_steps=10, 12 | kwargs={"patient_name": "adolescent#002"}, 13 | ) 14 | 15 | env = gymnasium.make("simglucose/adolescent2-v0", render_mode="human") 16 | observation, info = env.reset() 17 | for t in range(200): 18 | env.render() 19 | action = env.action_space.sample() 20 | observation, reward, terminated, truncated, info = env.step(action) 21 | print( 22 | f"Step {t}: observation {observation}, reward {reward}, terminated {terminated}, truncated {truncated}, info {info}" 23 | ) 24 | if terminated or truncated: 25 | print("Episode finished after {} timesteps".format(t + 1)) 26 | break 27 | 28 | 29 | if __name__ == "__main__": 30 | unittest.main() 31 | -------------------------------------------------------------------------------- /tests/test_pid_controller.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from simglucose.controller.pid_ctrller import PIDController 3 | from simglucose.simulation.user_interface import simulate 4 | from unittest.mock import patch 5 | import shutil 6 | import os 7 | import pandas as pd 8 | 9 | output_folder = os.path.join(os.path.dirname(__file__), 'results') 10 | 11 | 12 | class TestPIDController(unittest.TestCase): 13 | def setUp(self): 14 | pass 15 | 16 | @patch('builtins.input') 17 | def test_pid_controller(self, mock_input): 18 | pid_controller = PIDController(P=0.001, I=0.00001, D=0.001) 19 | # animation, parallel, save_path, sim_time, scenario, scenario random 20 | # seed, start_time, patients, sensor, sensor seed, insulin pump, 21 | # controller 22 | mock_input.side_effect = [ 23 | 'y', 'n', output_folder, '24', '1', '2', '6', '5', '1', 'd', '1', 24 | '1', '2', '1' 25 | ] 26 | results = simulate(controller=pid_controller) 27 | self.assertIsInstance(results, pd.DataFrame) 28 | 29 | def tearDown(self): 30 | shutil.rmtree(output_folder) 31 | 32 | 33 | if __name__ == '__main__': 34 | unittest.main() 35 | -------------------------------------------------------------------------------- /examples/run_gym.py: -------------------------------------------------------------------------------- 1 | import gym 2 | 3 | # Register gym environment. By specifying kwargs, 4 | # you are able to choose which patient to simulate. 5 | # patient_name must be 'adolescent#001' to 'adolescent#010', 6 | # or 'adult#001' to 'adult#010', or 'child#001' to 'child#010' 7 | from gym.envs.registration import register 8 | register( 9 | id='simglucose-adolescent2-v0', 10 | entry_point='simglucose.envs:T1DSimEnv', 11 | kwargs={'patient_name': 'adolescent#002'} 12 | ) 13 | 14 | env = gym.make('simglucose-adolescent2-v0') 15 | 16 | observation = env.reset() 17 | for t in range(100): 18 | env.render(mode='human') 19 | print(observation) 20 | # Action in the gym environment is a scalar 21 | # representing the basal insulin, which differs from 22 | # the regular controller action outside the gym 23 | # environment (a tuple (basal, bolus)). 24 | # In the perfect situation, the agent should be able 25 | # to control the glucose only through basal instead 26 | # of asking patient to take bolus 27 | action = env.action_space.sample() 28 | observation, reward, done, info = env.step(action) 29 | if done: 30 | print("Episode finished after {} timesteps".format(t + 1)) 31 | break 32 | -------------------------------------------------------------------------------- /examples/run_rllab.py: -------------------------------------------------------------------------------- 1 | from rllab.algos.ddpg import DDPG 2 | from rllab.envs.normalized_env import normalize 3 | from rllab.exploration_strategies.ou_strategy import OUStrategy 4 | from rllab.policies.deterministic_mlp_policy import DeterministicMLPPolicy 5 | from rllab.q_functions.continuous_mlp_q_function import ContinuousMLPQFunction 6 | from rllab.envs.gym_env import GymEnv 7 | from gym.envs.registration import register 8 | 9 | register( 10 | id='simglucose-adolescent2-v0', 11 | entry_point='simglucose.envs:T1DSimEnv', 12 | kwargs={'patient_name': 'adolescent#002'} 13 | ) 14 | 15 | env = GymEnv('simglucose-adolescent2-v0') 16 | env = normalize(env) 17 | 18 | policy = DeterministicMLPPolicy( 19 | env_spec=env.spec, 20 | # The neural network policy should have two hidden layers, each with 32 hidden units. 21 | hidden_sizes=(32, 32) 22 | ) 23 | 24 | es = OUStrategy(env_spec=env.spec) 25 | 26 | qf = ContinuousMLPQFunction(env_spec=env.spec) 27 | 28 | algo = DDPG( 29 | env=env, 30 | policy=policy, 31 | es=es, 32 | qf=qf, 33 | batch_size=32, 34 | max_path_length=100, 35 | epoch_length=3, 36 | min_pool_size=10000, 37 | n_epochs=1000, 38 | discount=0.99, 39 | scale_reward=0.01, 40 | qf_learning_rate=1e-3, 41 | policy_learning_rate=1e-4 42 | ) 43 | algo.train() 44 | -------------------------------------------------------------------------------- /simglucose/analysis/risk.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | def risk_index(BG, horizon): 4 | # BG is in mg/dL 5 | BG_to_compute = BG[-horizon:] 6 | risks =[risk(r) for r in BG_to_compute] 7 | LBGI = np.mean([r[0] for r in risks]) 8 | HBGI = np.mean([r[1] for r in risks]) 9 | RI = np.mean([r[2] for r in risks]) 10 | 11 | return (LBGI, HBGI, RI) 12 | 13 | def risk(BG): 14 | """ 15 | Risk is a percentage - ranging from 0 to 100%. 16 | The 20 and 600 mg/dl are just the values to which the risk formula was fit. 17 | The aim is to make the risk maximum when it is either 20 or 600. 18 | The units in the paper below are different (mmol/l), but in our units (mg/dl) these limits are 20 and 600. 19 | 20 | Reference, in particular see appendix for the derivation of risk: 21 | https://diabetesjournals.org/care/article/20/11/1655/21162/Symmetrization-of-the-Blood-Glucose-Measurement 22 | 23 | """ 24 | MIN_BG = 20.0 25 | MAX_BG = 600.0 26 | if BG <= MIN_BG: 27 | return (100.0, 0.0, 100.0) 28 | if BG >= MAX_BG: 29 | return (0.0, 100.0, 100.0) 30 | 31 | U = 1.509 * (np.log(BG)**1.084 - 5.381) 32 | 33 | ri = 10 * U**2 34 | 35 | rl, rh = 0.0, 0.0 36 | if U <= 0: 37 | rl = ri 38 | if U >= 0: 39 | rh = ri 40 | return (rl, rh, ri) 41 | -------------------------------------------------------------------------------- /simglucose/controller/base.py: -------------------------------------------------------------------------------- 1 | from collections import namedtuple 2 | 3 | Action = namedtuple('ctrller_action', ['basal', 'bolus']) 4 | 5 | 6 | class Controller(object): 7 | def __init__(self, init_state): 8 | self.init_state = init_state 9 | self.state = init_state 10 | 11 | def policy(self, observation, reward, done, **info): 12 | ''' 13 | Every controller must have this implementation! 14 | ---- 15 | Inputs: 16 | observation - a namedtuple defined in simglucose.simulation.env. It has 17 | CHO and CGM two entries. 18 | reward - current reward returned by environment 19 | done - True, game over. False, game continues 20 | info - additional information as key word arguments, 21 | simglucose.simulation.env.T1DSimEnv returns patient_name 22 | and sample_time 23 | ---- 24 | Output: 25 | action - a namedtuple defined at the beginning of this file. The 26 | controller action contains two entries: basal, bolus 27 | ''' 28 | raise NotImplementedError 29 | 30 | def reset(self): 31 | ''' 32 | Reset the controller state to inital state, must be implemented 33 | ''' 34 | raise NotImplementedError 35 | -------------------------------------------------------------------------------- /tests/test_gym.py: -------------------------------------------------------------------------------- 1 | import gym 2 | import unittest 3 | from simglucose.controller.basal_bolus_ctrller import BBController 4 | 5 | 6 | class TestGym(unittest.TestCase): 7 | def test_gym_random_agent(self): 8 | from gym.envs.registration import register 9 | register( 10 | id='simglucose-adolescent2-v0', 11 | entry_point='simglucose.envs:T1DSimEnv', 12 | kwargs={'patient_name': 'adolescent#002'} 13 | ) 14 | 15 | env = gym.make('simglucose-adolescent2-v0') 16 | ctrller = BBController() 17 | 18 | reward = 0 19 | done = False 20 | info = {'sample_time': 3, 21 | 'patient_name': 'adolescent#002', 22 | 'meal': 0} 23 | 24 | observation = env.reset() 25 | for t in range(200): 26 | env.render(mode='human') 27 | print(observation) 28 | # action = env.action_space.sample() 29 | ctrl_action = ctrller.policy(observation, reward, done, **info) 30 | action = ctrl_action.basal + ctrl_action.bolus 31 | observation, reward, done, info = env.step(action) 32 | if done: 33 | print("Episode finished after {} timesteps".format(t + 1)) 34 | break 35 | 36 | 37 | if __name__ == '__main__': 38 | unittest.main() 39 | -------------------------------------------------------------------------------- /simglucose/controller/pid_ctrller.py: -------------------------------------------------------------------------------- 1 | from .base import Controller 2 | from .base import Action 3 | import logging 4 | 5 | logger = logging.getLogger(__name__) 6 | 7 | 8 | class PIDController(Controller): 9 | def __init__(self, P=1, I=0, D=0, target=140): 10 | self.P = P 11 | self.I = I 12 | self.D = D 13 | self.target = target 14 | self.integrated_state = 0 15 | self.prev_state = 0 16 | 17 | def policy(self, observation, reward, done, **kwargs): 18 | sample_time = kwargs.get('sample_time') 19 | 20 | # BG is the only state for this PID controller 21 | bg = observation.CGM 22 | control_input = self.P * (bg - self.target) + \ 23 | self.I * self.integrated_state + \ 24 | self.D * (bg - self.prev_state) / sample_time 25 | 26 | logger.info('Control input: {}'.format(control_input)) 27 | 28 | # update the states 29 | self.prev_state = bg 30 | self.integrated_state += (bg - self.target) * sample_time 31 | logger.info('prev state: {}'.format(self.prev_state)) 32 | logger.info('integrated state: {}'.format(self.integrated_state)) 33 | 34 | # return the action 35 | action = Action(basal=control_input, bolus=0) 36 | return action 37 | 38 | def reset(self): 39 | self.integrated_state = 0 40 | self.prev_state = 0 41 | -------------------------------------------------------------------------------- /.github/workflows/python-package.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a variety of Python versions 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions 3 | 4 | name: Python package 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | pull_request: 10 | branches: [ master ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | strategy: 17 | matrix: 18 | python-version: ['3.9', '3.10', '3.11'] 19 | 20 | steps: 21 | - uses: actions/checkout@v2 22 | - name: Set up Python ${{ matrix.python-version }} 23 | uses: actions/setup-python@v2 24 | with: 25 | python-version: ${{ matrix.python-version }} 26 | - name: Install dependencies 27 | run: | 28 | python -m pip install --upgrade pip 29 | python -m pip install flake8 pytest 30 | pip install -e . 31 | - name: Lint with flake8 32 | run: | 33 | # stop the build if there are Python syntax errors or undefined names 34 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics 35 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide 36 | flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics 37 | - name: Test with pytest 38 | run: | 39 | pytest 40 | -------------------------------------------------------------------------------- /simglucose/params/Quest.csv: -------------------------------------------------------------------------------- 1 | Name,CR,CF,Age,TDI 2 | adolescent#001,12,15.0360283441,18,36.7339146827 3 | adolescent#002,5,13.1750891807,19,62.031042736 4 | adolescent#003,23,33.5259913553,15,24.2427803659 5 | adolescent#004,14,21.8224838663,17,35.2528665417 6 | adolescent#005,12,20.9084301999,16,34.0047207467 7 | adolescent#006,7,17.6966328214,14,49.5812925714 8 | adolescent#007,8,12.4931832305,16,43.638064 9 | adolescent#008,4,11.9355179763,14,63.3866974592 10 | adolescent#009,21,20.013934775,19,24.0781667718 11 | adolescent#010,14,31.8684843717,17,33.1735076937 12 | adult#001,10,8.77310657487,61,50.416652 13 | adult#002,8,9.21276345633,65,57.86877688 14 | adult#003,9,17.9345522688,27,56.4297186222 15 | adult#004,16,42.6533755134,66,33.8079423727 16 | adult#005,5,8.23126750783,52,68.315922352 17 | adult#006,10,18.21328135,26,61.38880928 18 | adult#007,22,26.1530845971,35,42.0066074109 19 | adult#008,13,12.2505850562,48,42.7787865846 20 | adult#009,5,7.64317583896,68,67.211482912 21 | adult#010,5,10.69260456,68,64.448546656 22 | child#001,25,42.7177301243,9,17.4729287744 23 | child#002,23,36.9153947641,9,18.1778368801 24 | child#003,22,31.0525488428,8,16.0218757795 25 | child#004,25,40.7235040865,12,19.8151389176 26 | child#005,7,33.6312561084,10,40.9345323552 27 | child#006,19,39.9807063565,8,20.2240173973 28 | child#007,8,24.9969862972,9,36.2128538724 29 | child#008,15,30.8823307429,10,21.4944284585 30 | child#009,25,35.3170388027,7,17.3942110251 31 | child#010,18,29.222745246,12,20.6516964887 32 | -------------------------------------------------------------------------------- /examples/run_multi_patient_multi_scenario.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | import gym 3 | import simglucose 4 | from simglucose.simulation.scenario import CustomScenario 5 | import numpy as np 6 | 7 | 8 | start_time = datetime(2018, 1, 1, 0, 0, 0) 9 | meal_scenario_1 = CustomScenario(start_time=start_time, scenario=[(1, 20)]) 10 | meal_scenario_2 = CustomScenario(start_time=start_time, scenario=[(3, 15)]) 11 | 12 | 13 | patient_name = [ 14 | "adult#001", 15 | "adult#002", 16 | "adult#003", 17 | "adult#004", 18 | "adult#005", 19 | "adult#006", 20 | "adult#007", 21 | "adult#008", 22 | "adult#009", 23 | "adult#010", 24 | ] 25 | 26 | gym.envs.register( 27 | id="env-v0", 28 | entry_point="simglucose.envs:T1DSimEnv", 29 | kwargs={ 30 | "patient_name": patient_name, 31 | "custom_scenario": [meal_scenario_1, meal_scenario_2], 32 | }, 33 | ) 34 | 35 | env = gym.make("env-v0") 36 | 37 | env.reset() 38 | 39 | min_insulin = env.action_space.low 40 | max_insulin = env.action_space.high 41 | 42 | observation = env.reset() 43 | for t in range(100): 44 | env.render(mode="human") 45 | # action = np.random.uniform(min_insulin, max_insulin) 46 | 47 | action = observation.CGM * 0.0005 48 | if observation.CGM < 120: 49 | action = 0 50 | 51 | # print(action) 52 | observation, reward, done, info = env.step(action) 53 | if done: 54 | print("Episode finished after {} timesteps".format(t + 1)) 55 | break 56 | -------------------------------------------------------------------------------- /examples/apply_customized_controller.py: -------------------------------------------------------------------------------- 1 | from simglucose.simulation.user_interface import simulate 2 | from simglucose.controller.base import Controller, Action 3 | 4 | 5 | class MyController(Controller): 6 | def __init__(self, init_state): 7 | self.init_state = init_state 8 | self.state = init_state 9 | 10 | def policy(self, observation, reward, done, **info): 11 | ''' 12 | Every controller must have this implementation! 13 | ---- 14 | Inputs: 15 | observation - a namedtuple defined in simglucose.simulation.env. For 16 | now, it only has one entry: blood glucose level measured 17 | by CGM sensor. 18 | reward - current reward returned by environment 19 | done - True, game over. False, game continues 20 | info - additional information as key word arguments, 21 | simglucose.simulation.env.T1DSimEnv returns patient_name 22 | and sample_time 23 | ---- 24 | Output: 25 | action - a namedtuple defined at the beginning of this file. The 26 | controller action contains two entries: basal, bolus 27 | ''' 28 | self.state = observation 29 | action = Action(basal=0, bolus=0) 30 | return action 31 | 32 | def reset(self): 33 | ''' 34 | Reset the controller state to inital state, must be implemented 35 | ''' 36 | self.state = self.init_state 37 | 38 | 39 | ctrller = MyController(0) 40 | simulate(controller=ctrller) 41 | -------------------------------------------------------------------------------- /simglucose/actuator/pump.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | import pkg_resources 3 | import logging 4 | import numpy as np 5 | 6 | INSULIN_PUMP_PARA_FILE = pkg_resources.resource_filename( 7 | 'simglucose', 'params/pump_params.csv') 8 | logger = logging.getLogger(__name__) 9 | 10 | 11 | class InsulinPump(object): 12 | U2PMOL = 6000 13 | 14 | def __init__(self, params): 15 | self._params = params 16 | 17 | @classmethod 18 | def withName(cls, name): 19 | pump_params = pd.read_csv(INSULIN_PUMP_PARA_FILE) 20 | params = pump_params.loc[pump_params.Name == name].squeeze() 21 | return cls(params) 22 | 23 | def bolus(self, amount): 24 | bol = amount * self.U2PMOL # convert from U/min to pmol/min 25 | bol = np.round(bol / self._params['inc_bolus'] 26 | ) * self._params['inc_bolus'] 27 | bol = bol / self.U2PMOL # convert from pmol/min to U/min 28 | bol = min(bol, self._params['max_bolus']) 29 | bol = max(bol, self._params['min_bolus']) 30 | return bol 31 | 32 | def basal(self, amount): 33 | bas = amount * self.U2PMOL # convert from U/min to pmol/min 34 | bas = np.round(bas / self._params['inc_basal'] 35 | ) * self._params['inc_basal'] 36 | bas = bas / self.U2PMOL # convert from pmol/min to U/min 37 | bas = min(bas, self._params['max_basal']) 38 | bas = max(bas, self._params['min_basal']) 39 | return bas 40 | 41 | def reset(self): 42 | logger.info('Resetting insulin pump ...') 43 | pass 44 | -------------------------------------------------------------------------------- /tests/test_report.py: -------------------------------------------------------------------------------- 1 | from simglucose.analysis.report import risk_index_trace 2 | from simglucose.sensor.cgm import CGMSensor 3 | from simglucose.simulation.rendering import Viewer 4 | from datetime import datetime 5 | import pandas as pd 6 | import unittest 7 | import logging 8 | import os 9 | 10 | logger = logging.getLogger(__name__) 11 | TESTDATA_FILENAME = os.path.join(os.path.dirname(__file__), 'sim_results.csv') 12 | 13 | 14 | class TestReport(unittest.TestCase): 15 | def setUp(self): 16 | self.df = pd.concat([pd.read_csv(TESTDATA_FILENAME, index_col=0)], keys=['test']) 17 | 18 | def test_risk_index_trace(self): 19 | BG = self.df.unstack(level=0).BG 20 | sample_time = CGMSensor.withName("Dexcom").sample_time 21 | ri_per_hour, ri_mean, fig, axes = risk_index_trace(BG, sample_time=sample_time) 22 | 23 | LBGI = ri_per_hour.transpose().LBGI 24 | HBGI = ri_per_hour.transpose().HBGI 25 | RI = ri_per_hour.transpose()["Risk Index"] 26 | 27 | self.assertEqual(LBGI.size, 48) 28 | self.assertEqual(round(LBGI.iloc[-1].test, 3), 0.843) 29 | self.assertEqual(round(LBGI.iloc[0].test, 3), 0.0) 30 | 31 | self.assertEqual(HBGI.size, 48) 32 | self.assertEqual(round(HBGI.iloc[-1].test,3), 0.0) 33 | self.assertEqual(round(HBGI.iloc[0].test,3), 2.755) 34 | 35 | self.assertEqual(RI.size, 48) 36 | self.assertEqual(round(RI.iloc[-1].test,3), 0.843) 37 | self.assertEqual(round(RI.iloc[0].test,3), 2.755) 38 | 39 | 40 | if __name__ == '__main__': 41 | unittest.main() 42 | -------------------------------------------------------------------------------- /simglucose/sensor/cgm.py: -------------------------------------------------------------------------------- 1 | # from .noise_gen import CGMNoiseGenerator 2 | from .noise_gen import CGMNoise 3 | import pandas as pd 4 | import logging 5 | import pkg_resources 6 | 7 | logger = logging.getLogger(__name__) 8 | SENSOR_PARA_FILE = pkg_resources.resource_filename( 9 | 'simglucose', 'params/sensor_params.csv') 10 | 11 | 12 | class CGMSensor(object): 13 | def __init__(self, params, seed=None): 14 | self._params = params 15 | self.name = params.Name 16 | self.sample_time = params.sample_time 17 | self.seed = seed 18 | self._last_CGM = 0 19 | 20 | @classmethod 21 | def withName(cls, name, **kwargs): 22 | sensor_params = pd.read_csv(SENSOR_PARA_FILE) 23 | params = sensor_params.loc[sensor_params.Name == name].squeeze() 24 | return cls(params, **kwargs) 25 | 26 | def measure(self, patient): 27 | if patient.t % self.sample_time == 0: 28 | BG = patient.observation.Gsub 29 | CGM = BG + next(self._noise_generator) 30 | CGM = max(CGM, self._params["min"]) 31 | CGM = min(CGM, self._params["max"]) 32 | self._last_CGM = CGM 33 | return CGM 34 | 35 | # Zero-Order Hold 36 | return self._last_CGM 37 | 38 | @property 39 | def seed(self): 40 | return self._seed 41 | 42 | @seed.setter 43 | def seed(self, seed): 44 | self._seed = seed 45 | self._noise_generator = CGMNoise(self._params, seed=seed) 46 | 47 | def reset(self): 48 | logger.debug('Resetting CGM sensor ...') 49 | self._noise_generator = CGMNoise(self._params, seed=self.seed) 50 | self._last_CGM = 0 51 | 52 | 53 | if __name__ == '__main__': 54 | pass 55 | -------------------------------------------------------------------------------- /tests/test_rllab.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from gym.envs.registration import register 3 | 4 | register( 5 | id='simglucose-adolescent1-v0', 6 | entry_point='simglucose.envs:T1DSimEnv', 7 | kwargs={ 8 | 'patient_name': 'adolescent#001' 9 | }) 10 | 11 | 12 | class testRLLab(unittest.TestCase): 13 | def test_rllab(self): 14 | try: 15 | from rllab.algos.ddpg import DDPG 16 | from rllab.envs.normalized_env import normalize 17 | from rllab.exploration_strategies.ou_strategy import OUStrategy 18 | from rllab.policies.deterministic_mlp_policy import DeterministicMLPPolicy 19 | from rllab.q_functions.continuous_mlp_q_function import ContinuousMLPQFunction 20 | from rllab.envs.gym_env import GymEnv 21 | except ImportError: 22 | print('rllab is not installed!') 23 | return None 24 | 25 | env = GymEnv('simglucose-adolescent1-v0') 26 | env = normalize(env) 27 | 28 | policy = DeterministicMLPPolicy( 29 | env_spec=env.spec, 30 | # The neural network policy should have two hidden layers, each 31 | # with 32 hidden units. 32 | hidden_sizes=(32, 32)) 33 | 34 | es = OUStrategy(env_spec=env.spec) 35 | 36 | qf = ContinuousMLPQFunction(env_spec=env.spec) 37 | 38 | algo = DDPG( 39 | env=env, 40 | policy=policy, 41 | es=es, 42 | qf=qf, 43 | batch_size=32, 44 | max_path_length=100, 45 | epoch_length=1000, 46 | min_pool_size=10000, 47 | n_epochs=5, 48 | discount=0.99, 49 | scale_reward=0.01, 50 | qf_learning_rate=1e-3, 51 | policy_learning_rate=1e-4) 52 | algo.train() 53 | -------------------------------------------------------------------------------- /tests/test_reward_fun.py: -------------------------------------------------------------------------------- 1 | import gym 2 | import unittest 3 | from simglucose.controller.basal_bolus_ctrller import BBController 4 | 5 | 6 | def custom_reward(BG_last_hour): 7 | if BG_last_hour[-1] > 180: 8 | return -1 9 | elif BG_last_hour[-1] < 70: 10 | return -2 11 | else: 12 | return 1 13 | 14 | 15 | class TestCustomReward(unittest.TestCase): 16 | def test_custom_reward(self): 17 | from gym.envs.registration import register 18 | register( 19 | id='simglucose-adolescent3-v0', 20 | entry_point='simglucose.envs:T1DSimEnv', 21 | kwargs={ 22 | 'patient_name': 'adolescent#003', 23 | 'reward_fun': custom_reward 24 | }) 25 | 26 | env = gym.make('simglucose-adolescent3-v0') 27 | ctrller = BBController() 28 | 29 | reward = 1 30 | done = False 31 | info = {'sample_time': 3, 'patient_name': 'adolescent#002', 'meal': 0} 32 | 33 | observation = env.reset() 34 | for t in range(200): 35 | env.render(mode='human') 36 | print(observation) 37 | # action = env.action_space.sample() 38 | ctrl_action = ctrller.policy(observation, reward, done, **info) 39 | action = ctrl_action.basal + ctrl_action.bolus 40 | observation, reward, done, info = env.step(action) 41 | print("Reward = {}".format(reward)) 42 | if observation.CGM > 180: 43 | self.assertEqual(reward, -1) 44 | elif observation.CGM < 70: 45 | self.assertEqual(reward, -2) 46 | else: 47 | self.assertEqual(reward, 1) 48 | if done: 49 | print("Episode finished after {} timesteps".format(t + 1)) 50 | break 51 | 52 | 53 | if __name__ == '__main__': 54 | unittest.main() 55 | -------------------------------------------------------------------------------- /tests/test_gym_custom_scenario.py: -------------------------------------------------------------------------------- 1 | import gym 2 | import unittest 3 | from simglucose.simulation.scenario import CustomScenario 4 | from simglucose.controller.basal_bolus_ctrller import BBController 5 | import datetime as dt 6 | 7 | 8 | class TestCustomScenario(unittest.TestCase): 9 | def test_custom_scenario(self): 10 | start_time = dt.datetime(2018, 1, 1, 0, 0, 0) 11 | 12 | meals = [(1,50),(2,10),(3,20)] 13 | meals_checked = [False for _ in range(len(meals))] 14 | 15 | current_pos = 0 16 | 17 | custom_meal_scenario = CustomScenario(start_time=start_time, scenario=meals) 18 | 19 | 20 | gym.envs.register( 21 | id='env-v0', 22 | entry_point='simglucose.envs:T1DSimEnv', 23 | kwargs={'patient_name': 'adult#001', 24 | 'custom_scenario': custom_meal_scenario} 25 | ) 26 | 27 | env = gym.make('env-v0') 28 | ctrller = BBController() 29 | 30 | reward = 0 31 | done = False 32 | 33 | sample_step = env.env.sensor.sample_time 34 | 35 | info = {'sample_time': sample_step, 36 | 'patient_name': 'adolescent#002', 37 | 'meal': 0} 38 | 39 | observation = env.reset() 40 | for t in range(61): 41 | env.render(mode='human') 42 | 43 | ctrl_action = ctrller.policy(observation, reward, done, **info) 44 | action = ctrl_action.basal + ctrl_action.bolus 45 | observation, reward, done, info = env.step(action) 46 | 47 | 48 | if info["meal"] > 0 and t*sample_step == (meals[current_pos][0]*60): 49 | meals_checked[current_pos] = True 50 | current_pos += 1 51 | 52 | 53 | if done: 54 | print("Episode finished after {} timesteps".format(t + 1)) 55 | break 56 | 57 | assert(all(meals_checked)) 58 | 59 | 60 | if __name__ == '__main__': 61 | unittest.main() -------------------------------------------------------------------------------- /simglucose/simulation/scenario.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from collections import namedtuple 3 | from datetime import datetime 4 | from datetime import timedelta 5 | 6 | logger = logging.getLogger(__name__) 7 | Action = namedtuple('scenario_action', ['meal']) 8 | 9 | 10 | class Scenario(object): 11 | def __init__(self, start_time): 12 | self.start_time = start_time 13 | 14 | def get_action(self, t): 15 | raise NotImplementedError 16 | 17 | def reset(self): 18 | raise NotImplementedError 19 | 20 | 21 | class CustomScenario(Scenario): 22 | def __init__(self, start_time, scenario): 23 | ''' 24 | scenario - a list of tuples (time, action), where time is a datetime or 25 | timedelta or double, action is a namedtuple defined by 26 | scenario.Action. When time is a timedelta, it is 27 | interpreted as the time of start_time + time. Time in double 28 | type is interpreted as time in timedelta with unit of hours 29 | ''' 30 | Scenario.__init__(self, start_time=start_time) 31 | self.scenario = scenario 32 | 33 | def get_action(self, t): 34 | if not self.scenario: 35 | return Action(meal=0) 36 | else: 37 | times, actions = tuple(zip(*self.scenario)) 38 | times2compare = [parseTime(time, self.start_time) for time in times] 39 | if t in times2compare: 40 | idx = times2compare.index(t) 41 | return Action(meal=actions[idx]) 42 | return Action(meal=0) 43 | 44 | def reset(self): 45 | pass 46 | 47 | 48 | def parseTime(time, start_time): 49 | if isinstance(time, (int, float)): 50 | t = start_time + timedelta(minutes=round(time * 60.0)) 51 | elif isinstance(time, timedelta): 52 | t_sec = time.total_seconds() 53 | t_min = round(t_sec / 60.0) 54 | t = start_time + timedelta(minutes=t_min) 55 | elif isinstance(time, datetime): 56 | t = time 57 | else: 58 | raise ValueError('Expect time to be int, float, timedelta, datetime') 59 | return t 60 | -------------------------------------------------------------------------------- /examples/advanced_tutorial.py: -------------------------------------------------------------------------------- 1 | from simglucose.simulation.env import T1DSimEnv 2 | from simglucose.controller.basal_bolus_ctrller import BBController 3 | from simglucose.sensor.cgm import CGMSensor 4 | from simglucose.actuator.pump import InsulinPump 5 | from simglucose.patient.t1dpatient import T1DPatient 6 | from simglucose.simulation.scenario_gen import RandomScenario 7 | from simglucose.simulation.scenario import CustomScenario 8 | from simglucose.simulation.sim_engine import SimObj, sim, batch_sim 9 | from datetime import timedelta 10 | from datetime import datetime 11 | 12 | # specify start_time as the beginning of today 13 | now = datetime.now() 14 | start_time = datetime.combine(now.date(), datetime.min.time()) 15 | 16 | # --------- Create Random Scenario -------------- 17 | # Specify results saving path 18 | path = './results' 19 | 20 | # Create a simulation environment 21 | patient = T1DPatient.withName('adolescent#001') 22 | sensor = CGMSensor.withName('Dexcom', seed=1) 23 | pump = InsulinPump.withName('Insulet') 24 | scenario = RandomScenario(start_time=start_time, seed=1) 25 | env = T1DSimEnv(patient, sensor, pump, scenario) 26 | 27 | # Create a controller 28 | controller = BBController() 29 | 30 | # Put them together to create a simulation object 31 | s1 = SimObj(env, controller, timedelta(days=1), animate=False, path=path) 32 | results1 = sim(s1) 33 | print(results1) 34 | 35 | # --------- Create Custom Scenario -------------- 36 | # Create a simulation environment 37 | patient = T1DPatient.withName('adolescent#001') 38 | sensor = CGMSensor.withName('Dexcom', seed=1) 39 | pump = InsulinPump.withName('Insulet') 40 | # custom scenario is a list of tuples (time, meal_size) 41 | scen = [(7, 45), (12, 70), (16, 15), (18, 80), (23, 10)] 42 | scenario = CustomScenario(start_time=start_time, scenario=scen) 43 | env = T1DSimEnv(patient, sensor, pump, scenario) 44 | 45 | # Create a controller 46 | controller = BBController() 47 | 48 | # Put them together to create a simulation object 49 | s2 = SimObj(env, controller, timedelta(days=1), animate=False, path=path) 50 | results2 = sim(s2) 51 | print(results2) 52 | 53 | 54 | # --------- batch simulation -------------- 55 | # Re-initialize simulation objects 56 | s1.reset() 57 | s2.reset() 58 | 59 | # create a list of SimObj, and call batch_sim 60 | s = [s1, s2] 61 | results = batch_sim(s, parallel=True) 62 | print(results) 63 | -------------------------------------------------------------------------------- /simglucose/simulation/sim_engine.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import time 3 | import os 4 | 5 | pathos = True 6 | try: 7 | from pathos.multiprocessing import ProcessPool as Pool 8 | except ImportError: 9 | print('You could install pathos to enable parallel simulation.') 10 | pathos = False 11 | 12 | logger = logging.getLogger(__name__) 13 | 14 | 15 | class SimObj(object): 16 | def __init__(self, 17 | env, 18 | controller, 19 | sim_time, 20 | animate=True, 21 | path=None): 22 | self.env = env 23 | self.controller = controller 24 | self.sim_time = sim_time 25 | self.animate = animate 26 | self._ctrller_kwargs = None 27 | self.path = path 28 | 29 | def simulate(self): 30 | self.controller.reset() 31 | obs, reward, done, info = self.env.reset() 32 | tic = time.time() 33 | while self.env.time < self.env.scenario.start_time + self.sim_time: 34 | if self.animate: 35 | self.env.render() 36 | action = self.controller.policy(obs, reward, done, **info) 37 | obs, reward, done, info = self.env.step(action) 38 | toc = time.time() 39 | logger.info('Simulation took {} seconds.'.format(toc - tic)) 40 | 41 | def results(self): 42 | return self.env.show_history() 43 | 44 | def save_results(self): 45 | df = self.results() 46 | if not os.path.isdir(self.path): 47 | os.makedirs(self.path) 48 | filename = os.path.join(self.path, str(self.env.patient.name) + '.csv') 49 | df.to_csv(filename) 50 | 51 | def reset(self): 52 | self.env.reset() 53 | self.controller.reset() 54 | 55 | 56 | def sim(sim_object): 57 | print("Process ID: {}".format(os.getpid())) 58 | print('Simulation starts ...') 59 | sim_object.simulate() 60 | sim_object.save_results() 61 | print('Simulation Completed!') 62 | return sim_object.results() 63 | 64 | 65 | def batch_sim(sim_instances, parallel=False): 66 | tic = time.time() 67 | if parallel and pathos: 68 | with Pool() as p: 69 | results = p.map(sim, sim_instances) 70 | else: 71 | if parallel and not pathos: 72 | print('Simulation is using single process even though parallel=True.') 73 | results = [sim(s) for s in sim_instances] 74 | toc = time.time() 75 | print('Simulation took {} sec.'.format(toc - tic)) 76 | return results 77 | -------------------------------------------------------------------------------- /tests/test_reset.py: -------------------------------------------------------------------------------- 1 | import gym 2 | import unittest 3 | from gym.envs.registration import register 4 | 5 | register( 6 | id='simglucose-adult2-v0', 7 | entry_point='simglucose.envs:T1DSimEnv', 8 | kwargs={'patient_name': 'adult#002'} 9 | ) 10 | 11 | 12 | class TestReset(unittest.TestCase): 13 | def test_reset_changes_observation_when_seed_is_fixed(self): 14 | env = gym.make('simglucose-adult2-v0') 15 | 16 | env.seed(0) 17 | observation0 = env.reset() 18 | start_time0 = env.env.scenario.start_time 19 | scenario0 = env.env.scenario.scenario 20 | 21 | observation1 = env.reset() 22 | start_time1 = env.env.scenario.start_time 23 | scenario1 = env.env.scenario.scenario 24 | 25 | self.assertNotEqual(observation0, observation1) 26 | self.assertNotEqual(start_time0, start_time1) 27 | self.assertNotEqual(scenario0, scenario1) 28 | 29 | def test_reset_change_is_deterministic_when_seed_is_fixed(self): 30 | env = gym.make('simglucose-adult2-v0') 31 | 32 | env.seed(0) 33 | observation0 = env.reset() 34 | start_time0 = env.env.scenario.start_time 35 | scenario0 = env.env.scenario.scenario 36 | 37 | observation1 = env.reset() 38 | start_time1 = env.env.scenario.start_time 39 | scenario1 = env.env.scenario.scenario 40 | 41 | env.seed(0) 42 | observation2 = env.reset() 43 | start_time2 = env.env.scenario.start_time 44 | scenario2 = env.env.scenario.scenario 45 | 46 | observation3 = env.reset() 47 | start_time3 = env.env.scenario.start_time 48 | scenario3 = env.env.scenario.scenario 49 | 50 | self.assertEqual(observation0, observation2) 51 | self.assertEqual(observation1, observation3) 52 | 53 | self.assertEqual(start_time0, start_time2) 54 | self.assertEqual(start_time1, start_time3) 55 | 56 | self.assertEqual(scenario0, scenario2) 57 | self.assertEqual(scenario1, scenario3) 58 | 59 | def test_reset_change_is_random_when_seed_is_different(self): 60 | env = gym.make('simglucose-adult2-v0') 61 | 62 | env.seed(0) 63 | observation0 = env.reset() 64 | start_time0 = env.env.scenario.start_time 65 | scenario0 = env.env.scenario.scenario 66 | 67 | observation1 = env.reset() 68 | start_time1 = env.env.scenario.start_time 69 | scenario1 = env.env.scenario.scenario 70 | 71 | env.seed(1) 72 | observation2 = env.reset() 73 | start_time2 = env.env.scenario.start_time 74 | scenario2 = env.env.scenario.scenario 75 | 76 | observation3 = env.reset() 77 | start_time3 = env.env.scenario.start_time 78 | scenario3 = env.env.scenario.scenario 79 | 80 | self.assertNotEqual(observation0, observation2) 81 | self.assertNotEqual(observation1, observation3) 82 | 83 | self.assertNotEqual(start_time0, start_time2) 84 | self.assertNotEqual(start_time1, start_time3) 85 | 86 | self.assertNotEqual(scenario0, scenario2) 87 | self.assertNotEqual(scenario1, scenario3) 88 | 89 | if __name__ == '__main__': 90 | unittest.main() 91 | -------------------------------------------------------------------------------- /simglucose/sensor/noise_gen.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from scipy.interpolate import interp1d 3 | import math 4 | from collections import deque 5 | import logging 6 | import matplotlib.pyplot as plt 7 | 8 | logger = logging.getLogger(__name__) 9 | 10 | 11 | def johnson_transform_SU(xi, lam, gamma, delta, x): 12 | return xi + lam * np.sinh((x - gamma) / delta) 13 | 14 | 15 | class CGMNoise(object): 16 | PRECOMPUTE = 10 # length of pre-compute noise sequence 17 | MDL_SAMPLE_TIME = 15 18 | 19 | def __init__(self, params, n=np.inf, seed=None): 20 | self._params = params 21 | self.seed = seed 22 | # self._noise15_gen = self._noise15_generator() 23 | self._noise15_gen = noise15_iter(params, seed=seed) 24 | self._noise_init = next(self._noise15_gen) 25 | 26 | self.n = n 27 | self.count = 0 28 | self.noise = deque() 29 | 30 | def _get_noise_seq(self): 31 | # To make the noise sequence continous, keep the last noise as the 32 | # beginning of the new sequence 33 | noise15 = [self._noise_init] 34 | noise15.extend([next(self._noise15_gen) 35 | for _ in range(self.PRECOMPUTE)]) 36 | self._noise_init = noise15[-1] 37 | 38 | noise15 = np.array(noise15) 39 | t15 = np.array(range(0, len(noise15))) * self.MDL_SAMPLE_TIME 40 | 41 | nsample = int(math.floor( 42 | self.PRECOMPUTE * self.MDL_SAMPLE_TIME / self._params["sample_time"])) + 1 43 | t = np.array(range(0, nsample)) * self._params["sample_time"] 44 | 45 | interp_f = interp1d(t15, noise15, kind='cubic') 46 | noise = interp_f(t) 47 | noise2return = deque(noise[1:]) 48 | 49 | # logger.debug('New noise sampled every 15 min:\n{}'.format(noise15)) 50 | # logger.debug('New noise sequence:\n{}'.format(noise2return)) 51 | 52 | # plt.plot(t15, noise15, 'o') 53 | # plt.plot(t, noise, '.-') 54 | # plt.show() 55 | 56 | return noise2return 57 | 58 | def __iter__(self): 59 | return self 60 | 61 | def __next__(self): 62 | if self.count < self.n: 63 | if len(self.noise) == 0: 64 | logger.debug('Generating a new noise sequence ...') 65 | self.noise = self._get_noise_seq() 66 | self.count += 1 67 | return self.noise.popleft() 68 | else: 69 | raise StopIteration() 70 | 71 | 72 | class noise15_iter: 73 | def __init__(self, params, seed=None, n=np.inf): 74 | self.seed = seed 75 | self.rand_gen = np.random.RandomState(self.seed) 76 | self._params = params 77 | self.n = n 78 | self.e = 0 79 | self.count = 0 80 | 81 | def __iter__(self): 82 | return self 83 | 84 | def __next__(self): 85 | if self.count == 0: 86 | self.e = self.rand_gen.randn() 87 | elif self.count < self.n: 88 | self.e = self._params["PACF"] * (self.e + self.rand_gen.randn()) 89 | else: 90 | raise StopIteration() 91 | eps = johnson_transform_SU(self._params["xi"], 92 | self._params["lambda"], 93 | self._params["gamma"], 94 | self._params["delta"], 95 | self.e) 96 | self.count += 1 97 | return eps 98 | -------------------------------------------------------------------------------- /simglucose/controller/basal_bolus_ctrller.py: -------------------------------------------------------------------------------- 1 | from .base import Controller 2 | from .base import Action 3 | import numpy as np 4 | import pandas as pd 5 | import pkg_resources 6 | import logging 7 | 8 | logger = logging.getLogger(__name__) 9 | CONTROL_QUEST = pkg_resources.resource_filename('simglucose', 10 | 'params/Quest.csv') 11 | PATIENT_PARA_FILE = pkg_resources.resource_filename( 12 | 'simglucose', 'params/vpatient_params.csv') 13 | 14 | 15 | class BBController(Controller): 16 | """ 17 | This is a Basal-Bolus Controller that is typically practiced by a Type-1 18 | Diabetes patient. The performance of this controller can serve as a 19 | baseline when developing a more advanced controller. 20 | """ 21 | def __init__(self, target=140): 22 | self.quest = pd.read_csv(CONTROL_QUEST) 23 | self.patient_params = pd.read_csv(PATIENT_PARA_FILE) 24 | self.target = target 25 | 26 | def policy(self, observation, reward, done, **kwargs): 27 | sample_time = kwargs.get('sample_time', 1) 28 | pname = kwargs.get('patient_name') 29 | meal = kwargs.get('meal') # unit: g/min 30 | 31 | action = self._bb_policy(pname, meal, observation.CGM, sample_time) 32 | return action 33 | 34 | def _bb_policy(self, name, meal, glucose, env_sample_time): 35 | """ 36 | Helper function to compute the basal and bolus amount. 37 | 38 | The basal insulin is based on the insulin amount to keep the blood 39 | glucose in the steady state when there is no (meal) disturbance. 40 | basal = u2ss (pmol/(L*kg)) * body_weight (kg) / 6000 (U/min) 41 | 42 | The bolus amount is computed based on the current glucose level, the 43 | target glucose level, the patient's correction factor and the patient's 44 | carbohydrate ratio. 45 | bolus = ((carbohydrate / carbohydrate_ratio) + 46 | (current_glucose - target_glucose) / correction_factor) 47 | / sample_time 48 | NOTE the bolus computed from the above formula is in unit U. The 49 | simulator only accepts insulin rate. Hence the bolus is converted to 50 | insulin rate. 51 | """ 52 | if any(self.quest.Name.str.match(name)): 53 | quest = self.quest[self.quest.Name.str.match(name)] 54 | params = self.patient_params[self.patient_params.Name.str.match( 55 | name)] 56 | u2ss = params.u2ss.values.item() # unit: pmol/(L*kg) 57 | BW = params.BW.values.item() # unit: kg 58 | else: 59 | quest = pd.DataFrame([['Average', 1 / 15, 1 / 50, 50, 30]], 60 | columns=['Name', 'CR', 'CF', 'TDI', 'Age']) 61 | u2ss = 1.43 # unit: pmol/(L*kg) 62 | BW = 57.0 # unit: kg 63 | 64 | basal = u2ss * BW / 6000 # unit: U/min 65 | if meal > 0: 66 | logger.info('Calculating bolus ...') 67 | logger.info(f'Meal = {meal} g/min') 68 | logger.info(f'glucose = {glucose}') 69 | bolus = ( 70 | (meal * env_sample_time) / quest.CR.values + (glucose > 150) * 71 | (glucose - self.target) / quest.CF.values).item() # unit: U 72 | else: 73 | bolus = 0 # unit: U 74 | 75 | # This is to convert bolus in total amount (U) to insulin rate (U/min). 76 | # The simulation environment does not treat basal and bolus 77 | # differently. The unit of Action.basal and Action.bolus are the same 78 | # (U/min). 79 | bolus = bolus / env_sample_time # unit: U/min 80 | return Action(basal=basal, bolus=bolus) 81 | 82 | def reset(self): 83 | pass 84 | -------------------------------------------------------------------------------- /simglucose/simulation/scenario_gen.py: -------------------------------------------------------------------------------- 1 | from simglucose.simulation.scenario import Action, Scenario 2 | import numpy as np 3 | from scipy.stats import truncnorm 4 | from datetime import datetime 5 | import logging 6 | 7 | logger = logging.getLogger(__name__) 8 | 9 | 10 | class RandomScenario(Scenario): 11 | def __init__(self, start_time, seed=None): 12 | Scenario.__init__(self, start_time=start_time) 13 | self.seed = seed 14 | 15 | def get_action(self, t): 16 | # t must be datetime.datetime object 17 | delta_t = t - datetime.combine(t.date(), datetime.min.time()) 18 | t_sec = delta_t.total_seconds() 19 | 20 | if t_sec < 1: 21 | logger.info('Creating new one day scenario ...') 22 | self.scenario = self.create_scenario() 23 | 24 | t_min = np.floor(t_sec / 60.0) 25 | 26 | if t_min in self.scenario['meal']['time']: 27 | logger.info('Time for meal!') 28 | idx = self.scenario['meal']['time'].index(t_min) 29 | return Action(meal=self.scenario['meal']['amount'][idx]) 30 | else: 31 | return Action(meal=0) 32 | 33 | def create_scenario(self): 34 | scenario = {'meal': {'time': [], 'amount': []}} 35 | 36 | # Probability of taking each meal 37 | # [breakfast, snack1, lunch, snack2, dinner, snack3] 38 | prob = [0.95, 0.3, 0.95, 0.3, 0.95, 0.3] 39 | time_lb = np.array([5, 9, 10, 14, 16, 20]) * 60 40 | time_ub = np.array([9, 10, 14, 16, 20, 23]) * 60 41 | time_mu = np.array([7, 9.5, 12, 15, 18, 21.5]) * 60 42 | time_sigma = np.array([60, 30, 60, 30, 60, 30]) 43 | amount_mu = [45, 10, 70, 10, 80, 10] 44 | amount_sigma = [10, 5, 10, 5, 10, 5] 45 | 46 | for p, tlb, tub, tbar, tsd, mbar, msd in zip(prob, time_lb, time_ub, 47 | time_mu, time_sigma, 48 | amount_mu, amount_sigma): 49 | if self.random_gen.rand() < p: 50 | tmeal = np.round( 51 | truncnorm.rvs(a=(tlb - tbar) / tsd, 52 | b=(tub - tbar) / tsd, 53 | loc=tbar, 54 | scale=tsd, 55 | random_state=self.random_gen)) 56 | scenario['meal']['time'].append(tmeal) 57 | scenario['meal']['amount'].append( 58 | max(round(self.random_gen.normal(mbar, msd)), 0)) 59 | 60 | return scenario 61 | 62 | def reset(self): 63 | self.random_gen = np.random.RandomState(self.seed) 64 | self.scenario = self.create_scenario() 65 | 66 | @property 67 | def seed(self): 68 | return self._seed 69 | 70 | @seed.setter 71 | def seed(self, seed): 72 | self._seed = seed 73 | self.reset() 74 | 75 | 76 | if __name__ == '__main__': 77 | from datetime import time 78 | from datetime import timedelta 79 | import copy 80 | now = datetime.now() 81 | t0 = datetime.combine(now.date(), time(6, 0, 0, 0)) 82 | t = copy.deepcopy(t0) 83 | sim_time = timedelta(days=2) 84 | 85 | scenario = RandomScenario(seed=1) 86 | m = [] 87 | T = [] 88 | while t < t0 + sim_time: 89 | action = scenario.get_action(t) 90 | m.append(action.meal) 91 | T.append(t) 92 | t += timedelta(minutes=1) 93 | 94 | import matplotlib.pyplot as plt 95 | import matplotlib.dates as mdates 96 | plt.plot(T, m) 97 | ax = plt.gca() 98 | ax.xaxis.set_minor_locator(mdates.AutoDateLocator()) 99 | ax.xaxis.set_minor_formatter(mdates.DateFormatter('%H:%M\n')) 100 | ax.xaxis.set_major_locator(mdates.DayLocator()) 101 | ax.xaxis.set_major_formatter(mdates.DateFormatter('\n%b %d')) 102 | plt.show() 103 | -------------------------------------------------------------------------------- /examples/results/2017-12-31_17-46-32/performance_stats.csv: -------------------------------------------------------------------------------- 1 | ,70<=BG<=180,BG>180,BG<70,BG>250,BG<50,LBGI,HBGI,Risk Index 2 | adolescent#001,88.98128898128898,11.01871101871102,0.0,0.0,0.0,0.12278183457051267,2.619576277742741,2.7423581123132537 3 | adolescent#002,28.690228690228693,41.16424116424117,30.14553014553015,33.264033264033266,22.66112266112266,12.88169895204457,12.564171239995975,25.44587019204054 4 | adolescent#003,37.42203742203743,38.46153846153847,24.116424116424117,17.463617463617464,17.463617463617464,8.255743445513504,6.199270189665567,14.45501363517907 5 | adolescent#004,37.42203742203743,38.46153846153847,24.116424116424117,24.532224532224532,20.16632016632017,9.204200565470739,6.692877570337627,15.897078135808366 6 | adolescent#005,38.04573804573805,38.66943866943867,23.284823284823286,24.532224532224532,14.760914760914762,6.571404005548895,7.104593528582134,13.67599753413103 7 | adolescent#006,70.68607068607069,29.313929313929314,0.0,8.93970893970894,0.0,0.10345798230438749,4.923581592362817,5.027039574667204 8 | adolescent#007,35.13513513513514,38.87733887733888,25.987525987525988,29.521829521829524,24.532224532224532,29.721108344261697,9.437516600268502,39.1586249445302 9 | adolescent#008,41.16424116424117,40.12474012474013,18.711018711018713,28.8981288981289,13.305613305613306,6.4108551579331605,9.827376137428967,16.238231295362127 10 | adolescent#009,50.72765072765073,33.679833679833685,15.592515592515593,8.731808731808732,0.0,2.3982737889291834,4.034206808820304,6.432480597749488 11 | adolescent#010,40.95634095634096,35.343035343035346,23.7006237006237,12.681912681912683,16.008316008316008,7.0188735671613625,5.526203984793224,12.545077551954588 12 | adult#001,67.77546777546777,32.224532224532226,0.0,0.0,0.0,1.4741662203818233,3.8165059035785913,5.290672123960415 13 | adult#002,75.67567567567568,24.324324324324326,0.0,0.0,0.0,0.6835295455225354,2.654046452604969,3.337575998127505 14 | adult#003,63.409563409563404,36.590436590436596,0.0,14.345114345114347,0.0,0.6611814636559613,5.594033422714297,6.255214886370259 15 | adult#004,60.91476091476091,39.08523908523909,0.0,18.711018711018713,0.0,0.3122185401198936,6.983486154270229,7.295704694390121 16 | adult#005,58.62785862785863,34.0956340956341,7.276507276507277,4.365904365904366,0.0,1.1796463737190985,4.20432058724939,5.383966960968488 17 | adult#006,47.19334719334719,39.5010395010395,13.305613305613306,7.0686070686070686,0.0,2.000136905931577,5.702097013945899,7.702233919877475 18 | adult#007,59.04365904365905,33.056133056133056,7.900207900207901,0.0,0.0,1.3595700125066872,3.3883912479292753,4.747961260435963 19 | adult#008,74.01247401247402,25.987525987525988,0.0,0.0,0.0,0.7311480808595261,2.5293549188077664,3.260502999667292 20 | adult#009,35.96673596673597,35.96673596673597,28.06652806652807,3.5343035343035343,18.503118503118504,13.06319931177901,4.9020546387361845,17.965253950515194 21 | adult#010,61.95426195426196,38.04573804573805,0.0,18.295218295218298,0.0,0.6569039881417948,6.352062419981345,7.00896640812314 22 | child#001,27.027027027027028,32.016632016632016,40.95634095634096,21.62162162162162,18.295218295218298,7.440350221822669,6.075927550099028,13.516277771921699 23 | child#002,23.7006237006237,32.640332640332645,43.65904365904366,19.542619542619544,34.71933471933472,26.976921037377778,4.204457668866692,31.181378706244466 24 | child#003,28.482328482328484,34.303534303534306,37.21413721413722,28.8981288981289,27.027027027027028,35.81131572069946,8.057887085060079,43.86920280575954 25 | child#004,27.234927234927238,24.94802494802495,47.81704781704782,0.6237006237006237,21.205821205821206,11.431382288852689,2.019833757257887,13.451216046110574 26 | child#005,93.34719334719335,6.652806652806653,0.0,0.0,0.0,0.47247842414790525,2.0392466796370923,2.5117251037849977 27 | child#006,18.503118503118504,35.13513513513514,46.36174636174636,26.611226611226613,37.62993762993763,28.62437209641833,7.476046651338808,36.10041874775713 28 | child#007,74.42827442827443,25.571725571725572,0.0,11.434511434511435,0.0,0.09334999392783182,3.5977282097493752,3.6910782036772067 29 | child#008,14.760914760914762,40.12474012474013,45.11434511434512,36.38253638253639,38.25363825363826,32.12259043776102,16.676570024400114,48.79916046216113 30 | child#009,73.38877338877339,26.611226611226613,0.0,15.384615384615385,0.0,0.16181405725181883,5.332218634654862,5.494032691906681 31 | child#010,40.12474012474013,35.75883575883576,24.116424116424117,25.363825363825367,13.097713097713099,5.7209765324272555,6.911809899641286,12.632786432068542 32 | -------------------------------------------------------------------------------- /tests/test_sim_engine.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from pandas.testing import assert_frame_equal 3 | import pandas as pd 4 | from simglucose.simulation.env import T1DSimEnv 5 | from simglucose.controller.basal_bolus_ctrller import BBController 6 | from simglucose.sensor.cgm import CGMSensor 7 | from simglucose.actuator.pump import InsulinPump 8 | from simglucose.patient.t1dpatient import T1DPatient 9 | from simglucose.simulation.scenario_gen import RandomScenario 10 | from simglucose.simulation.scenario import CustomScenario 11 | from simglucose.simulation.sim_engine import SimObj, sim, batch_sim 12 | from datetime import timedelta 13 | from datetime import datetime 14 | import os 15 | import logging 16 | import shutil 17 | 18 | logger = logging.getLogger(__name__) 19 | 20 | TESTDATA_FILENAME = os.path.join(os.path.dirname(__file__), "sim_results.csv") 21 | save_folder = os.path.join(os.path.dirname(__file__), "results") 22 | 23 | 24 | class TestSimEngine(unittest.TestCase): 25 | def test_batch_sim(self): 26 | # specify start_time as the beginning of today 27 | now = datetime.now() 28 | start_time = datetime.combine(now.date(), datetime.min.time()) 29 | 30 | # --------- Create Random Scenario -------------- 31 | # Create a simulation environment 32 | patient = T1DPatient.withName("adolescent#001") 33 | sensor = CGMSensor.withName("Dexcom", seed=1) 34 | pump = InsulinPump.withName("Insulet") 35 | scenario = RandomScenario(start_time=start_time, seed=1) 36 | env = T1DSimEnv(patient, sensor, pump, scenario) 37 | 38 | # Create a controller 39 | controller = BBController() 40 | 41 | # Put them together to create a simulation object 42 | s1 = SimObj(env, controller, timedelta(days=2), animate=True, path=save_folder) 43 | results1 = sim(s1) 44 | 45 | # --------- Create Custom Scenario -------------- 46 | # Create a simulation environment 47 | patient = T1DPatient.withName("adolescent#001") 48 | sensor = CGMSensor.withName("Dexcom", seed=1) 49 | pump = InsulinPump.withName("Insulet") 50 | # custom scenario is a list of tuples (time, meal_size) 51 | scen = [(7, 45), (12, 70), (16, 15), (18, 80), (23, 10)] 52 | scenario = CustomScenario(start_time=start_time, scenario=scen) 53 | env = T1DSimEnv(patient, sensor, pump, scenario) 54 | 55 | # Create a controller 56 | controller = BBController() 57 | 58 | # Put them together to create a simulation object 59 | s2 = SimObj(env, controller, timedelta(days=2), animate=False, path=save_folder) 60 | results2 = sim(s2) 61 | 62 | # --------- batch simulation -------------- 63 | s1.reset() 64 | s2.reset() 65 | s1.animate = False 66 | s = [s1, s2] 67 | results_para = batch_sim(s, parallel=True) 68 | 69 | s1.reset() 70 | s2.reset() 71 | s = [s1, s2] 72 | results_serial = batch_sim(s, parallel=False) 73 | 74 | assert_frame_equal(results_para[0], results1) 75 | assert_frame_equal(results_para[1], results2) 76 | for r1, r2 in zip(results_para, results_serial): 77 | assert_frame_equal(r1, r2) 78 | 79 | def test_results_consistency(self): 80 | # Test data 81 | results_exp = pd.read_csv(TESTDATA_FILENAME, index_col=0) 82 | results_exp.index = pd.to_datetime(results_exp.index) 83 | 84 | # specify start_time as the beginning of today 85 | start_time = datetime(2018, 1, 1, 0, 0, 0) 86 | 87 | # --------- Create Random Scenario -------------- 88 | # Create a simulation environment 89 | patient = T1DPatient.withName("adolescent#001") 90 | sensor = CGMSensor.withName("Dexcom", seed=1) 91 | pump = InsulinPump.withName("Insulet") 92 | scenario = RandomScenario(start_time=start_time, seed=1) 93 | env = T1DSimEnv(patient, sensor, pump, scenario) 94 | 95 | # Create a controller 96 | controller = BBController() 97 | 98 | # Put them together to create a simulation object 99 | s = SimObj(env, controller, timedelta(days=2), animate=False, path=save_folder) 100 | results = sim(s) 101 | assert_frame_equal(results, results_exp) 102 | 103 | def tearDown(self): 104 | shutil.rmtree(os.path.join(os.path.dirname(__file__), "results")) 105 | 106 | 107 | if __name__ == "__main__": 108 | unittest.main() 109 | -------------------------------------------------------------------------------- /simglucose/simulation/rendering.py: -------------------------------------------------------------------------------- 1 | import matplotlib.pyplot as plt 2 | import matplotlib.dates as mdates 3 | import logging 4 | from datetime import timedelta 5 | 6 | logger = logging.getLogger(__name__) 7 | 8 | 9 | class Viewer(object): 10 | def __init__(self, start_time, patient_name, figsize=None): 11 | self.start_time = start_time 12 | self.patient_name = patient_name 13 | self.fig, self.axes, self.lines = self.initialize() 14 | self.update() 15 | 16 | def initialize(self): 17 | plt.ion() 18 | fig, axes = plt.subplots(4) 19 | 20 | axes[0].set_ylabel('BG (mg/dL)') 21 | axes[1].set_ylabel('CHO (g/min)') 22 | axes[2].set_ylabel('Insulin (U/min)') 23 | axes[3].set_ylabel('Risk Index') 24 | 25 | lineBG, = axes[0].plot([], [], label='BG') 26 | lineCGM, = axes[0].plot([], [], label='CGM') 27 | lineCHO, = axes[1].plot([], [], label='CHO') 28 | lineIns, = axes[2].plot([], [], label='Insulin') 29 | lineLBGI, = axes[3].plot([], [], label='Hypo Risk') 30 | lineHBGI, = axes[3].plot([], [], label='Hyper Risk') 31 | lineRI, = axes[3].plot([], [], label='Risk Index') 32 | 33 | lines = [lineBG, lineCGM, lineCHO, lineIns, lineLBGI, lineHBGI, lineRI] 34 | 35 | axes[0].set_ylim([70, 180]) 36 | axes[1].set_ylim([-5, 30]) 37 | axes[2].set_ylim([-0.5, 1]) 38 | axes[3].set_ylim([0, 5]) 39 | 40 | for ax in axes: 41 | ax.set_xlim( 42 | [self.start_time, self.start_time + timedelta(hours=3)]) 43 | ax.legend() 44 | 45 | # Plot zone patches 46 | axes[0].axhspan(70, 180, alpha=0.3, color='limegreen', lw=0) 47 | axes[0].axhspan(50, 70, alpha=0.3, color='red', lw=0) 48 | axes[0].axhspan(0, 50, alpha=0.3, color='darkred', lw=0) 49 | axes[0].axhspan(180, 250, alpha=0.3, color='red', lw=0) 50 | axes[0].axhspan(250, 1000, alpha=0.3, color='darkred', lw=0) 51 | 52 | axes[0].tick_params(labelbottom=False) 53 | axes[1].tick_params(labelbottom=False) 54 | axes[2].tick_params(labelbottom=False) 55 | axes[3].xaxis.set_minor_locator(mdates.AutoDateLocator()) 56 | axes[3].xaxis.set_minor_formatter(mdates.DateFormatter('%H:%M\n')) 57 | axes[3].xaxis.set_major_locator(mdates.DayLocator()) 58 | axes[3].xaxis.set_major_formatter(mdates.DateFormatter('\n%b %d')) 59 | 60 | axes[0].set_title(self.patient_name) 61 | 62 | return fig, axes, lines 63 | 64 | def update(self): 65 | self.fig.canvas.draw() 66 | self.fig.canvas.flush_events() 67 | 68 | def render(self, data): 69 | self.lines[0].set_xdata(data.index.values) 70 | self.lines[0].set_ydata(data['BG'].values) 71 | 72 | self.lines[1].set_xdata(data.index.values) 73 | self.lines[1].set_ydata(data['CGM'].values) 74 | 75 | self.axes[0].draw_artist(self.axes[0].patch) 76 | self.axes[0].draw_artist(self.lines[0]) 77 | self.axes[0].draw_artist(self.lines[1]) 78 | 79 | adjust_ylim(self.axes[0], min(min(data['BG']), min(data['CGM'])), 80 | max(max(data['BG']), max(data['CGM']))) 81 | adjust_xlim(self.axes[0], data.index[-1]) 82 | 83 | self.lines[2].set_xdata(data.index.values) 84 | self.lines[2].set_ydata(data['CHO'].values) 85 | 86 | self.axes[1].draw_artist(self.axes[1].patch) 87 | self.axes[1].draw_artist(self.lines[2]) 88 | 89 | adjust_ylim(self.axes[1], min(data['CHO']), max(data['CHO'])) 90 | adjust_xlim(self.axes[1], data.index[-1]) 91 | 92 | self.lines[3].set_xdata(data.index.values) 93 | self.lines[3].set_ydata(data['insulin'].values) 94 | 95 | self.axes[2].draw_artist(self.axes[2].patch) 96 | self.axes[2].draw_artist(self.lines[3]) 97 | adjust_ylim(self.axes[2], min(data['insulin']), max(data['insulin'])) 98 | adjust_xlim(self.axes[2], data.index[-1]) 99 | 100 | self.lines[4].set_xdata(data.index.values) 101 | self.lines[4].set_ydata(data['LBGI'].values) 102 | 103 | self.lines[5].set_xdata(data.index.values) 104 | self.lines[5].set_ydata(data['HBGI'].values) 105 | 106 | self.lines[6].set_xdata(data.index.values) 107 | self.lines[6].set_ydata(data['Risk'].values) 108 | 109 | self.axes[3].draw_artist(self.axes[3].patch) 110 | self.axes[3].draw_artist(self.lines[4]) 111 | self.axes[3].draw_artist(self.lines[5]) 112 | self.axes[3].draw_artist(self.lines[6]) 113 | adjust_ylim(self.axes[3], min(data['Risk']), max(data['Risk'])) 114 | adjust_xlim(self.axes[3], data.index[-1], xlabel=True) 115 | 116 | self.update() 117 | 118 | def close(self): 119 | plt.close(self.fig) 120 | 121 | 122 | def adjust_ylim(ax, ymin, ymax): 123 | ylim = ax.get_ylim() 124 | update = False 125 | 126 | if ymin < ylim[0]: 127 | y1 = ymin - 0.1 * abs(ymin) 128 | update = True 129 | else: 130 | y1 = ylim[0] 131 | 132 | if ymax > ylim[1]: 133 | y2 = ymax + 0.1 * abs(ymax) 134 | update = True 135 | else: 136 | y2 = ylim[1] 137 | 138 | if update: 139 | ax.set_ylim([y1, y2]) 140 | for spine in ax.spines.values(): 141 | ax.draw_artist(spine) 142 | ax.draw_artist(ax.yaxis) 143 | 144 | 145 | def adjust_xlim(ax, timemax, xlabel=False): 146 | xlim = mdates.num2date(ax.get_xlim()) 147 | update = False 148 | 149 | # remove timezone awareness to make them comparable 150 | timemax = timemax.replace(tzinfo=None) 151 | xlim[0] = xlim[0].replace(tzinfo=None) 152 | xlim[1] = xlim[1].replace(tzinfo=None) 153 | 154 | if timemax > xlim[1] - timedelta(minutes=30): 155 | xmax = xlim[1] + timedelta(hours=6) 156 | update = True 157 | 158 | if update: 159 | ax.set_xlim([xlim[0], xmax]) 160 | for spine in ax.spines.values(): 161 | ax.draw_artist(spine) 162 | ax.draw_artist(ax.xaxis) 163 | if xlabel: 164 | ax.xaxis.set_minor_locator(mdates.AutoDateLocator()) 165 | ax.xaxis.set_minor_formatter(mdates.DateFormatter('%H:%M\n')) 166 | ax.xaxis.set_major_locator(mdates.DayLocator()) 167 | ax.xaxis.set_major_formatter(mdates.DateFormatter('\n%b %d')) 168 | -------------------------------------------------------------------------------- /simglucose/simulation/env.py: -------------------------------------------------------------------------------- 1 | from simglucose.patient.t1dpatient import Action 2 | from simglucose.analysis.risk import risk_index 3 | import pandas as pd 4 | from datetime import timedelta 5 | import logging 6 | from collections import namedtuple 7 | from simglucose.simulation.rendering import Viewer 8 | 9 | try: 10 | from rllab.envs.base import Step 11 | except ImportError: 12 | _Step = namedtuple("Step", ["observation", "reward", "done", "info"]) 13 | 14 | def Step(observation, reward, done, **kwargs): 15 | """ 16 | Convenience method creating a namedtuple with the results of the 17 | environment.step method. 18 | Put extra diagnostic info in the kwargs 19 | """ 20 | return _Step(observation, reward, done, kwargs) 21 | 22 | 23 | Observation = namedtuple("Observation", ["CGM"]) 24 | logger = logging.getLogger(__name__) 25 | 26 | 27 | def risk_diff(BG_last_hour): 28 | if len(BG_last_hour) < 2: 29 | return 0 30 | else: 31 | _, _, risk_current = risk_index([BG_last_hour[-1]], 1) 32 | _, _, risk_prev = risk_index([BG_last_hour[-2]], 1) 33 | return risk_prev - risk_current 34 | 35 | 36 | class T1DSimEnv(object): 37 | def __init__(self, patient, sensor, pump, scenario): 38 | self.patient = patient 39 | self.sensor = sensor 40 | self.pump = pump 41 | self.scenario = scenario 42 | self._reset() 43 | 44 | @property 45 | def time(self): 46 | return self.scenario.start_time + timedelta(minutes=self.patient.t) 47 | 48 | def mini_step(self, action): 49 | # current action 50 | patient_action = self.scenario.get_action(self.time) 51 | basal = self.pump.basal(action.basal) 52 | bolus = self.pump.bolus(action.bolus) 53 | insulin = basal + bolus 54 | CHO = patient_action.meal 55 | patient_mdl_act = Action(insulin=insulin, CHO=CHO) 56 | 57 | # State update 58 | self.patient.step(patient_mdl_act) 59 | 60 | # next observation 61 | BG = self.patient.observation.Gsub 62 | CGM = self.sensor.measure(self.patient) 63 | 64 | return CHO, insulin, BG, CGM 65 | 66 | def step(self, action, reward_fun=risk_diff): 67 | """ 68 | action is a namedtuple with keys: basal, bolus 69 | """ 70 | CHO = 0.0 71 | insulin = 0.0 72 | BG = 0.0 73 | CGM = 0.0 74 | 75 | for _ in range(int(self.sample_time)): 76 | # Compute moving average as the sample measurements 77 | tmp_CHO, tmp_insulin, tmp_BG, tmp_CGM = self.mini_step(action) 78 | CHO += tmp_CHO / self.sample_time 79 | insulin += tmp_insulin / self.sample_time 80 | BG += tmp_BG / self.sample_time 81 | CGM += tmp_CGM / self.sample_time 82 | 83 | # Compute risk index 84 | horizon = 1 85 | LBGI, HBGI, risk = risk_index([BG], horizon) 86 | 87 | # Record current action 88 | self.CHO_hist.append(CHO) 89 | self.insulin_hist.append(insulin) 90 | 91 | # Record next observation 92 | self.time_hist.append(self.time) 93 | self.BG_hist.append(BG) 94 | self.CGM_hist.append(CGM) 95 | self.risk_hist.append(risk) 96 | self.LBGI_hist.append(LBGI) 97 | self.HBGI_hist.append(HBGI) 98 | 99 | # Compute reward, and decide whether game is over 100 | window_size = int(60 / self.sample_time) 101 | BG_last_hour = self.CGM_hist[-window_size:] 102 | reward = reward_fun(BG_last_hour) 103 | done = BG < 10 or BG > 600 104 | obs = Observation(CGM=CGM) 105 | 106 | return Step( 107 | observation=obs, 108 | reward=reward, 109 | done=done, 110 | sample_time=self.sample_time, 111 | patient_name=self.patient.name, 112 | meal=CHO, 113 | patient_state=self.patient.state, 114 | time=self.time, 115 | bg=BG, 116 | lbgi=LBGI, 117 | hbgi=HBGI, 118 | risk=risk, 119 | ) 120 | 121 | def _reset(self): 122 | self.sample_time = self.sensor.sample_time 123 | self.viewer = None 124 | 125 | BG = self.patient.observation.Gsub 126 | horizon = 1 127 | LBGI, HBGI, risk = risk_index([BG], horizon) 128 | CGM = self.sensor.measure(self.patient) 129 | self.time_hist = [self.scenario.start_time] 130 | self.BG_hist = [BG] 131 | self.CGM_hist = [CGM] 132 | self.risk_hist = [risk] 133 | self.LBGI_hist = [LBGI] 134 | self.HBGI_hist = [HBGI] 135 | self.CHO_hist = [] 136 | self.insulin_hist = [] 137 | 138 | def reset(self): 139 | self.patient.reset() 140 | self.sensor.reset() 141 | self.pump.reset() 142 | self.scenario.reset() 143 | self._reset() 144 | CGM = self.sensor.measure(self.patient) 145 | obs = Observation(CGM=CGM) 146 | return Step( 147 | observation=obs, 148 | reward=0, 149 | done=False, 150 | sample_time=self.sample_time, 151 | patient_name=self.patient.name, 152 | meal=0, 153 | patient_state=self.patient.state, 154 | time=self.time, 155 | bg=self.BG_hist[0], 156 | lbgi=self.LBGI_hist[0], 157 | hbgi=self.HBGI_hist[0], 158 | risk=self.risk_hist[0], 159 | ) 160 | 161 | def render(self, close=False): 162 | if close: 163 | self._close_viewer() 164 | return 165 | 166 | if self.viewer is None: 167 | self.viewer = Viewer(self.scenario.start_time, self.patient.name) 168 | 169 | self.viewer.render(self.show_history()) 170 | 171 | def _close_viewer(self): 172 | if self.viewer is not None: 173 | self.viewer.close() 174 | self.viewer = None 175 | 176 | def show_history(self): 177 | df = pd.DataFrame() 178 | df["Time"] = pd.Series(self.time_hist) 179 | df["BG"] = pd.Series(self.BG_hist) 180 | df["CGM"] = pd.Series(self.CGM_hist) 181 | df["CHO"] = pd.Series(self.CHO_hist) 182 | df["insulin"] = pd.Series(self.insulin_hist) 183 | df["LBGI"] = pd.Series(self.LBGI_hist) 184 | df["HBGI"] = pd.Series(self.HBGI_hist) 185 | df["Risk"] = pd.Series(self.risk_hist) 186 | df = df.set_index("Time") 187 | return df 188 | -------------------------------------------------------------------------------- /simglucose/envs/simglucose_gym_env.py: -------------------------------------------------------------------------------- 1 | from simglucose.simulation.env import T1DSimEnv as _T1DSimEnv 2 | from simglucose.patient.t1dpatient import T1DPatient 3 | from simglucose.sensor.cgm import CGMSensor 4 | from simglucose.actuator.pump import InsulinPump 5 | from simglucose.simulation.scenario_gen import RandomScenario 6 | from simglucose.controller.base import Action 7 | import numpy as np 8 | import pkg_resources 9 | import gym 10 | from gym import spaces 11 | from gym.utils import seeding 12 | from datetime import datetime 13 | import gymnasium 14 | 15 | 16 | PATIENT_PARA_FILE = pkg_resources.resource_filename( 17 | "simglucose", "params/vpatient_params.csv" 18 | ) 19 | 20 | 21 | class T1DSimEnv(gym.Env): 22 | """ 23 | A wrapper of simglucose.simulation.env.T1DSimEnv to support gym API 24 | """ 25 | 26 | metadata = {"render.modes": ["human"]} 27 | 28 | SENSOR_HARDWARE = "Dexcom" 29 | INSULIN_PUMP_HARDWARE = "Insulet" 30 | 31 | def __init__( 32 | self, patient_name=None, custom_scenario=None, reward_fun=None, seed=None 33 | ): 34 | """ 35 | patient_name must be 'adolescent#001' to 'adolescent#010', 36 | or 'adult#001' to 'adult#010', or 'child#001' to 'child#010' 37 | """ 38 | # have to hard code the patient_name, gym has some interesting 39 | # error when choosing the patient 40 | if patient_name is None: 41 | patient_name = ["adolescent#001"] 42 | 43 | self.patient_name = patient_name 44 | self.reward_fun = reward_fun 45 | self.np_random, _ = seeding.np_random(seed=seed) 46 | self.custom_scenario = custom_scenario 47 | self.env, _, _, _ = self._create_env() 48 | 49 | def _step(self, action: float): 50 | # This gym only controls basal insulin 51 | act = Action(basal=action, bolus=0) 52 | if self.reward_fun is None: 53 | return self.env.step(act) 54 | return self.env.step(act, reward_fun=self.reward_fun) 55 | 56 | def _raw_reset(self): 57 | return self.env.reset() 58 | 59 | def _reset(self): 60 | self.env, _, _, _ = self._create_env() 61 | obs, _, _, _ = self.env.reset() 62 | return obs 63 | 64 | def _seed(self, seed=None): 65 | self.np_random, seed1 = seeding.np_random(seed=seed) 66 | self.env, seed2, seed3, seed4 = self._create_env() 67 | return [seed1, seed2, seed3, seed4] 68 | 69 | def _create_env(self): 70 | # Derive a random seed. This gets passed as a uint, but gets 71 | # checked as an int elsewhere, so we need to keep it below 72 | # 2**31. 73 | seed2 = seeding.hash_seed(self.np_random.randint(0, 1000)) % 2**31 74 | seed3 = seeding.hash_seed(seed2 + 1) % 2**31 75 | seed4 = seeding.hash_seed(seed3 + 1) % 2**31 76 | 77 | hour = self.np_random.randint(low=0.0, high=24.0) 78 | start_time = datetime(2018, 1, 1, hour, 0, 0) 79 | 80 | if isinstance(self.patient_name, list): 81 | patient_name = self.np_random.choice(self.patient_name) 82 | patient = T1DPatient.withName(patient_name, random_init_bg=True, seed=seed4) 83 | else: 84 | patient = T1DPatient.withName( 85 | self.patient_name, random_init_bg=True, seed=seed4 86 | ) 87 | 88 | if isinstance(self.custom_scenario, list): 89 | scenario = self.np_random.choice(self.custom_scenario) 90 | else: 91 | scenario = ( 92 | RandomScenario(start_time=start_time, seed=seed3) 93 | if self.custom_scenario is None 94 | else self.custom_scenario 95 | ) 96 | 97 | sensor = CGMSensor.withName(self.SENSOR_HARDWARE, seed=seed2) 98 | pump = InsulinPump.withName(self.INSULIN_PUMP_HARDWARE) 99 | env = _T1DSimEnv(patient, sensor, pump, scenario) 100 | return env, seed2, seed3, seed4 101 | 102 | def _render(self, mode="human", close=False): 103 | self.env.render(close=close) 104 | 105 | def _close(self): 106 | super()._close() 107 | self.env._close_viewer() 108 | 109 | @property 110 | def action_space(self): 111 | ub = self.env.pump._params["max_basal"] 112 | return spaces.Box(low=0, high=ub, shape=(1,)) 113 | 114 | @property 115 | def observation_space(self): 116 | return spaces.Box(low=0, high=1000, shape=(1,)) 117 | 118 | @property 119 | def max_basal(self): 120 | return self.env.pump._params["max_basal"] 121 | 122 | 123 | class T1DSimGymnaisumEnv(gymnasium.Env): 124 | metadata = {"render_modes": ["human"], "render_fps": 60} 125 | MAX_BG = 1000 126 | 127 | def __init__( 128 | self, 129 | patient_name=None, 130 | custom_scenario=None, 131 | reward_fun=None, 132 | seed=None, 133 | render_mode=None, 134 | ) -> None: 135 | super().__init__() 136 | self.render_mode = render_mode 137 | self.env = T1DSimEnv( 138 | patient_name=patient_name, 139 | custom_scenario=custom_scenario, 140 | reward_fun=reward_fun, 141 | seed=seed, 142 | ) 143 | self.observation_space = gymnasium.spaces.Box( 144 | low=0, high=self.MAX_BG, shape=(1,), dtype=np.float32 145 | ) 146 | self.action_space = gymnasium.spaces.Box( 147 | low=0, high=self.env.max_basal, shape=(1,), dtype=np.float32 148 | ) 149 | 150 | def step(self, action): 151 | obs, reward, done, info = self.env.step(action) 152 | # Truncated will be controlled by TimeLimit wrapper when registering the env. 153 | # For example, 154 | # register( 155 | # id="simglucose/adolescent2-v0", 156 | # entry_point="simglucose.envs:T1DSimGymnaisumEnv", 157 | # max_episode_steps=10, 158 | # kwargs={"patient_name": "adolescent#002"}, 159 | # ) 160 | # Once the max_episode_steps is set, the truncated value will be overridden. 161 | truncated = False 162 | return np.array([obs.CGM], dtype=np.float32), reward, done, truncated, info 163 | 164 | def reset(self, seed=None, options=None): 165 | super().reset(seed=seed) 166 | obs, _, _, info = self.env._raw_reset() 167 | return np.array([obs.CGM], dtype=np.float32), info 168 | 169 | def render(self): 170 | if self.render_mode == "human": 171 | self.env.render() 172 | 173 | def close(self): 174 | self.env.close() 175 | -------------------------------------------------------------------------------- /definitions_of_vpatient_parameters.md: -------------------------------------------------------------------------------- 1 | ## Definitions of Parameters 2 | 3 | Below is my attempt at describing all of the parameters in ```vpatient_params.csv```. They were infered from a combination of 2 papers and inspecting the codebase itself it the parameter names were not perfectly clear. I tried to keep them in order of appearance in the ```vpatient_params.csv```. The original Meal Simulation Model of the Glucose-Insulin System (for T2D and Normal Adults) provides a lot more detail of the parameters, there are some parameters not discussed in that paper, and I needed to get the information from the UVA/PADOVA Type 1 Diabetes Simulator: New Features paper. 4 | 5 | 1. [Meal Simulation Model of the Glucose-Insulin System](https://ieeexplore.ieee.org/abstract/document/4303268/references#references) 6 | 2. [The UVA/PADOVA Type 1 Diabetes Simulator: New Features](https://journals.sagepub.com/doi/10.1177/1932296813514502) 7 | 8 | I would be happy if others improve, clean, or correct some of the definitions below. 9 | 10 | ### Definitions 11 | 12 | * BW (kg) - Body Weight 13 | * EGPb (mg/kg/min) - Edogenous Glucose Production, basal steady-state edogenous production, b suffix denotes basal state. 14 | * $EGP_b = EGP(0)$ 15 | * $EGP(t) = k_{p1} - k_{p2} * G_{p}(t) - k_{p3} * I_{d}(t) - k_{p4} * I_{p0}(t)$ 16 | * EGPb = Ub + Eb <- in normal subjects this is 0. $EGP_b = U_{b} + E_{b}$ 17 | * Ub (mg/kg/min) - Glucose utilization $U(0) = U_{b}$ 18 | * Eb (mg/kg/min) - Renal excretion $E(0) = E_{b}$ 19 | * Gb (mg/dl) - plasma Glucose concentration, b suffix denotes basal state: randomly generated from the joint distribution with an average of 120 mg/dl. In S2008, Gb was randomly generated from the joint distribution with an average of 140 mg/dl (chosen to reflect the knowledge available to the authors at that time). 20 | * Gb = Gp/Vg 21 | * Gp (mg/kg) - glucoses masses in plasma and glucose masses in rapidly equilibrating tissues $G_p(0) = G_{pb}$ 22 | * Gt (mg/kg) - slowly equilibrating tissues $G_{t}(0) = G_{tb}$ 23 | * Vg (dl/kg) - the distribution volume of glucose $V_{G}$ 24 | * Ib (pmol/l) - Insulin plasma concentration, b suffix denotes basal state. $I(0) = I_{b}$ 25 | * Ib = Ip/Vi 26 | * Ip (pmol/l) - inslin masses in plasma $I_{pb} = I_{p}(0)$ 27 | * Vi (l/kg) - distribution volume of insulin $V_{I}$ 28 | * kabs ($min^{-1}$) - Rate of appearance process, parameter - $k_{abs}$ is the rate constant of intestinal (gut) absorption 29 | * kmax ($min^{-1}$) - Rate of appearance process, parameter - $k_{max}$ 30 | * kmin ($min^{-1}$)- Rate of appearance process, parameter - $k_{min}$ 31 | * b (dimensionless) - Rate of appearance process, parameter 32 | * d (dimensionless) - Rate of appearance process, parameter 33 | * Vg (dl/kg) - Distribution volume of glucose, $V_{G}$ 34 | * Vi (l/kg) - Distribution volume of insulin, $V_{I}$ 35 | * Ipb (pmol/kg) - insulin masses in plasma, b suffix denotes basal state. $I_{pb} = I_{p}(0)$ 36 | * Vmx (mg/kg/min per pmol/l) - Utilization parameter quantifying peripheral insulin action, $V_{mx}$ 37 | * Km0 (mg/kg) - Utilization parameter 38 | * k2 ($min^{-1}$) - Glucose kinetics process, rate parameter (rates $G_p$ variable) 39 | * k1 ($min^{-1}$) - Glucose kinetics process, rate parameter (rates $G_t$ variable) 40 | * p2u ($min^{-1}$) - Utilization parameter 41 | * m1 ($min^{-1}$) - Insulin kenetics process, rate parameter (rates $I_l$ variables) 42 | * m5 (min*kg/pmol) - Insulin kenetics process, rate parameter (rates ) 43 | * CL - I don't see this used in the code and no mention of it in the papers, not sure what I'm missing with this one. 44 | * HEb (dimensionless) - Insulin kenectics process, Hepatic extraction of insulin, i.e., the insulin flux which leaves the liver irreversibly divided by the total insulin flux leaving the liver, b suffix denotes basal state. 45 | * $HE_b = HE(0) = -m5*S(0) + m6$ 46 | * S (pmol/kg/min) - insulin secretion, I guess this is assumed to be 0 in T1D hence why there is no S parameter, (probably bad assumption) 47 | * Therefore, in T1D, $m_6$ would be redundant because $HE_b = m_6$ (based on my assumption probably not correct) 48 | * m2 ($min^{-1}$) - Insulin kenetics rate parameter (rates $I_l$ variable) 49 | * $m_2 = -m_4/HE_{b}$, if assuming S = 0 50 | * $m_2 = (\frac{S_b}{I_{pb}}-\frac{m_4}{1-HE_{b}})*\frac{1-HE_{b}}{HE_{b}}$ 51 | * m4 ($min^{-1}$) - Insulin kenetics rate parameter (rates $I_p$ variable), corresponds to peripheral degradation and has been assumed linear 52 | * $m_4 = \frac{2}{5}*\frac{S_{b}}{I_{pb}}*(1-HE_b)$, generally 53 | * m30 (min^{-1}) - Insulin kenetics process, This really means the initial m3 value because m3 varies over time. 54 | * $m_3(0) = \frac{HE_{b}*m_{1}}{1 - HE_{b}}$ 55 | * $m_3(t) = \frac{HE(t)*m_{1}}{1 - HE(t)}$ 56 | * Ilb (pmol/kg) - insulin masses in liver, b suffix denotes basal state. $I_{lb} = I_{l}(0)$ 57 | * ki ($mg/kg/min$) - Edogenous production process, rate parameter accounting for delay between insulin signal and insulin action 58 | * kp2 ($min^{-1}$) - Edogenous production process, $k_{p2}$ liver (hepatic) glucose effectiveness 59 | * kp3 ($mg/kg/min per pmol/l$) - Edogenous production process, $k_{p3}$ parameter governing amplitude of insulin action on the liver, quantifying hepatic insulin action. 60 | * f (dimensionless) - Rate of Appearance process, (see Variables not included) 61 | * Gpb(mg/kg) - glucoses masses in plasma and glucose masses in rapidly equilibrating tissues, b indicates basal level. 62 | * $G_{pb} = G_{p}(0)$ 63 | * ke1 ($min^{-1}$) - Renal Excretion processs, used in $E(t) = k_{e1} * [G_{p}(t)-k_{e2}]$ if $G_{p}(t) > k_{e2}$, else $E(t) = 0$ 64 | * ke2 (mg/kg) - Renal Excretion processs, used in $E(t) = k_{e1} * [G_{p}(t)-k_{e2}]$ if $G_{p}(t) > k_{e2}$, else $E(t) = 0$ 65 | * Fsnc (mg/kg/min) - I believe this is a typo of $F_{cns}$ where I assume cns means central nervous system. This corresponds to glucose utilization by the brain and erythrocytes. 66 | * $U_{ii}(t) = F_{cns}$ 67 | * Gtb - I do not think this is used in the codebase, $G_{tb} = G_{t}(0)$ is at basal steady state 68 | * $G_{tb} = \frac{F_{cns}-EGP_{b} + k_{1}*G_{pb}}{k_{2}}$ 69 | * Vm0 (mg/kg/min) - Utilization process, $V_{m0}$ maximum utilization by tissue at basal insulin 70 | * $V_{m0} = \frac{(EGP_{b}-F_{cns})*(K_{m0}+G_{tb})}{G_{tb}}$ 71 | * Rdb - can't find reference, not used in code base 72 | * PCRb - can't find reference, not used in code base 73 | #### Subcutaneous Insulin Kinetics 74 | There are four equations that define insulin kinetics, the below variables define how these are calculated 75 | * Equations from papers: 76 | * $\dot{I}_{sc1} = -(k_{d} + k_{a1})*I_{sc1}(t) + IIR(t)$ 77 | * $I_{sc1}(0) = I_{sc1ss}$, see isclss below 78 | * $\dot{I}_{sc2} = k_{d}*I_{sc1}(t) - k_{a2}*I_{sc2}(t)$ 79 | * $I_{sc2}(0) = I_{sc2ss}$, see isc2ss below 80 | * kd - used in calculation of subcutaneous insulin kinetics $k_{d}$ (see above) 81 | * ksc - used in calculation of subcutaneous glucose $k_{sc}$ (see above) 82 | * ka1 - used in calculation of subcutaneous insulin kinetics $k_{a1}$ (see above) 83 | * ka2 - used in calculation of subcutaneous insulin kinetics $k_{a2}$ (see above) 84 | * doskempt - I assume this refers to $k_{empt}$ which is the rate constant of gastric emptying, which is a nonlinear function of $Q_{sto}$ 85 | * $Q_{sto}$ (mg) - is the amount of glucose in the stomach 86 | * u2ss - used for calculating basal insulin in ```t1dpatient.py``` see ```basal = p._params.u2ss * p._params.BW / 6000 # U/min``` 87 | * isclss - $I_{sc1}(0) = I_{sc1ss}$, the first steady state of subcutaneous insulin parameter 88 | * isc2ss - $I_{sc2}(0) = I_{sc2ss}$, the second steady state of subcutaneous insulin parameter 89 | * sp1 - could not find in code base 90 | * patient_history - could not find in code base 91 | 92 | #### Variables/Equations not included: 93 | These variable are worth noting because the above variables directly impact below, and do not make sense without the context. 94 | * $Ra(t)$ (mg/kg/min) - the glucose rate of apperance in plasma 95 | * $Ra(t) = \frac{f*k_{abs}*Q_{gut}(t)}{BW}$ 96 | * $k_{p1}$ (mg/kg/min) - is the extrapolated EGP at zero glucose and insulin 97 | -------------------------------------------------------------------------------- /simglucose/patient/t1dpatient.py: -------------------------------------------------------------------------------- 1 | from .base import Patient 2 | import numpy as np 3 | from scipy.integrate import ode 4 | import pandas as pd 5 | from collections import namedtuple 6 | import logging 7 | import pkg_resources 8 | 9 | logger = logging.getLogger(__name__) 10 | 11 | Action = namedtuple("patient_action", ["CHO", "insulin"]) 12 | Observation = namedtuple("observation", ["Gsub"]) 13 | 14 | PATIENT_PARA_FILE = pkg_resources.resource_filename( 15 | "simglucose", "params/vpatient_params.csv" 16 | ) 17 | 18 | 19 | class T1DPatient(Patient): 20 | SAMPLE_TIME = 1 # min 21 | EAT_RATE = 5 # g/min CHO 22 | 23 | def __init__(self, params, init_state=None, random_init_bg=False, seed=None, t0=0): 24 | """ 25 | T1DPatient constructor. 26 | Inputs: 27 | - params: a pandas sequence 28 | - init_state: customized initial state. 29 | If not specified, load the default initial state in 30 | params.iloc[2:15] 31 | - t0: simulation start time, it is 0 by default 32 | """ 33 | self._params = params 34 | self._init_state = init_state 35 | self.random_init_bg = random_init_bg 36 | self._seed = seed 37 | self.t0 = t0 38 | self.reset() 39 | 40 | @classmethod 41 | def withID(cls, patient_id, **kwargs): 42 | """ 43 | Construct patient by patient_id 44 | id are integers from 1 to 30. 45 | 1 - 10: adolescent#001 - adolescent#010 46 | 11 - 20: adult#001 - adult#001 47 | 21 - 30: child#001 - child#010 48 | """ 49 | patient_params = pd.read_csv(PATIENT_PARA_FILE) 50 | params = patient_params.iloc[patient_id - 1, :] 51 | return cls(params, **kwargs) 52 | 53 | @classmethod 54 | def withName(cls, name, **kwargs): 55 | """ 56 | Construct patient by name. 57 | Names can be 58 | adolescent#001 - adolescent#010 59 | adult#001 - adult#001 60 | child#001 - child#010 61 | """ 62 | patient_params = pd.read_csv(PATIENT_PARA_FILE) 63 | params = patient_params.loc[patient_params.Name == name].squeeze() 64 | return cls(params, **kwargs) 65 | 66 | @property 67 | def state(self): 68 | return self._odesolver.y 69 | 70 | @property 71 | def t(self): 72 | return self._odesolver.t 73 | 74 | @property 75 | def sample_time(self): 76 | return self.SAMPLE_TIME 77 | 78 | def step(self, action): 79 | # Convert announcing meal to the meal amount to eat at the moment 80 | to_eat = self._announce_meal(action.CHO) 81 | action = action._replace(CHO=to_eat) 82 | 83 | # Detect eating or not and update last digestion amount 84 | if action.CHO > 0 and self._last_action.CHO <= 0: 85 | logger.info("t = {}, patient starts eating ...".format(self.t)) 86 | self._last_Qsto = self.state[0] + self.state[1] # unit: mg 87 | self._last_foodtaken = 0 # unit: g 88 | self.is_eating = True 89 | 90 | if to_eat > 0: 91 | logger.debug("t = {}, patient eats {} g".format(self.t, action.CHO)) 92 | 93 | if self.is_eating: 94 | self._last_foodtaken += action.CHO # g 95 | 96 | # Detect eating ended 97 | if action.CHO <= 0 and self._last_action.CHO > 0: 98 | logger.info("t = {}, Patient finishes eating!".format(self.t)) 99 | self.is_eating = False 100 | 101 | # Update last input 102 | self._last_action = action 103 | 104 | # ODE solver 105 | self._odesolver.set_f_params( 106 | action, self._params, self._last_Qsto, self._last_foodtaken 107 | ) 108 | if self._odesolver.successful(): 109 | self._odesolver.integrate(self._odesolver.t + self.sample_time) 110 | else: 111 | logger.error("ODE solver failed!!") 112 | raise 113 | 114 | @staticmethod 115 | def model(t, x, action, params, last_Qsto, last_foodtaken): 116 | dxdt = np.zeros(13) 117 | d = action.CHO * 1000 # g -> mg 118 | insulin = action.insulin * 6000 / params.BW # U/min -> pmol/kg/min 119 | basal = params.u2ss * params.BW / 6000 # U/min 120 | 121 | # Glucose in the stomach 122 | qsto = x[0] + x[1] 123 | # NOTE: Dbar is in unit mg, hence last_foodtaken needs to be converted 124 | # from mg to g. See https://github.com/jxx123/simglucose/issues/41 for 125 | # details. 126 | Dbar = last_Qsto + last_foodtaken * 1000 # unit: mg 127 | 128 | # Stomach solid 129 | dxdt[0] = -params.kmax * x[0] + d 130 | 131 | if Dbar > 0: 132 | aa = 5 / (2 * Dbar * (1 - params.b)) 133 | cc = 5 / (2 * Dbar * params.d) 134 | kgut = params.kmin + (params.kmax - params.kmin) / 2 * ( 135 | np.tanh(aa * (qsto - params.b * Dbar)) 136 | - np.tanh(cc * (qsto - params.d * Dbar)) 137 | + 2 138 | ) 139 | else: 140 | kgut = params.kmax 141 | 142 | # stomach liquid 143 | dxdt[1] = params.kmax * x[0] - x[1] * kgut 144 | 145 | # intestine 146 | dxdt[2] = kgut * x[1] - params.kabs * x[2] 147 | 148 | # Rate of appearance 149 | Rat = params.f * params.kabs * x[2] / params.BW 150 | # Glucose Production 151 | EGPt = params.kp1 - params.kp2 * x[3] - params.kp3 * x[8] 152 | # Glucose Utilization 153 | Uiit = params.Fsnc 154 | 155 | # renal excretion 156 | if x[3] > params.ke2: 157 | Et = params.ke1 * (x[3] - params.ke2) 158 | else: 159 | Et = 0 160 | 161 | # glucose kinetics 162 | # plus dextrose IV injection input u[2] if needed 163 | dxdt[3] = max(EGPt, 0) + Rat - Uiit - Et - params.k1 * x[3] + params.k2 * x[4] 164 | dxdt[3] = (x[3] >= 0) * dxdt[3] 165 | 166 | Vmt = params.Vm0 + params.Vmx * x[6] 167 | Kmt = params.Km0 168 | Uidt = Vmt * x[4] / (Kmt + x[4]) 169 | dxdt[4] = -Uidt + params.k1 * x[3] - params.k2 * x[4] 170 | dxdt[4] = (x[4] >= 0) * dxdt[4] 171 | 172 | # insulin kinetics 173 | dxdt[5] = ( 174 | -(params.m2 + params.m4) * x[5] 175 | + params.m1 * x[9] 176 | + params.ka1 * x[10] 177 | + params.ka2 * x[11] 178 | ) # plus insulin IV injection u[3] if needed 179 | It = x[5] / params.Vi 180 | dxdt[5] = (x[5] >= 0) * dxdt[5] 181 | 182 | # insulin action on glucose utilization 183 | dxdt[6] = -params.p2u * x[6] + params.p2u * (It - params.Ib) 184 | 185 | # insulin action on production 186 | dxdt[7] = -params.ki * (x[7] - It) 187 | 188 | dxdt[8] = -params.ki * (x[8] - x[7]) 189 | 190 | # insulin in the liver (pmol/kg) 191 | dxdt[9] = -(params.m1 + params.m30) * x[9] + params.m2 * x[5] 192 | dxdt[9] = (x[9] >= 0) * dxdt[9] 193 | 194 | # subcutaneous insulin kinetics 195 | dxdt[10] = insulin - (params.ka1 + params.kd) * x[10] 196 | dxdt[10] = (x[10] >= 0) * dxdt[10] 197 | 198 | dxdt[11] = params.kd * x[10] - params.ka2 * x[11] 199 | dxdt[11] = (x[11] >= 0) * dxdt[11] 200 | 201 | # subcutaneous glucose 202 | dxdt[12] = -params.ksc * x[12] + params.ksc * x[3] 203 | dxdt[12] = (x[12] >= 0) * dxdt[12] 204 | 205 | if action.insulin > basal: 206 | logger.debug("t = {}, injecting insulin: {}".format(t, action.insulin)) 207 | 208 | return dxdt 209 | 210 | @property 211 | def observation(self): 212 | """ 213 | return the observation from patient 214 | for now, only the subcutaneous glucose level is returned 215 | TODO: add heart rate as an observation 216 | """ 217 | GM = self.state[12] # subcutaneous glucose (mg/kg) 218 | Gsub = GM / self._params.Vg 219 | observation = Observation(Gsub=Gsub) 220 | return observation 221 | 222 | def _announce_meal(self, meal): 223 | """ 224 | patient announces meal. 225 | The announced meal will be added to self.planned_meal 226 | The meal is consumed in self.EAT_RATE 227 | The function will return the amount to eat at current time 228 | """ 229 | self.planned_meal += meal 230 | if self.planned_meal > 0: 231 | to_eat = min(self.EAT_RATE, self.planned_meal) 232 | self.planned_meal -= to_eat 233 | self.planned_meal = max(0, self.planned_meal) 234 | else: 235 | to_eat = 0 236 | return to_eat 237 | 238 | @property 239 | def seed(self): 240 | return self._seed 241 | 242 | @seed.setter 243 | def seed(self, seed): 244 | self._seed = seed 245 | self.reset() 246 | 247 | def reset(self): 248 | """ 249 | Reset the patient state to default intial state 250 | """ 251 | if self._init_state is None: 252 | self.init_state = np.copy(self._params.iloc[2:15].values) 253 | else: 254 | self.init_state = self._init_state 255 | 256 | self.random_state = np.random.RandomState(self.seed) 257 | if self.random_init_bg: 258 | # Only randomize glucose related states, x4, x5, and x13 259 | mean = [ 260 | 1.0 * self.init_state[3], 261 | 1.0 * self.init_state[4], 262 | 1.0 * self.init_state[12], 263 | ] 264 | cov = np.diag( 265 | [ 266 | 0.1 * self.init_state[3], 267 | 0.1 * self.init_state[4], 268 | 0.1 * self.init_state[12], 269 | ] 270 | ) 271 | bg_init = self.random_state.multivariate_normal(mean, cov) 272 | self.init_state[3] = 1.0 * bg_init[0] 273 | self.init_state[4] = 1.0 * bg_init[1] 274 | self.init_state[12] = 1.0 * bg_init[2] 275 | 276 | self._last_Qsto = self.init_state[0] + self.init_state[1] 277 | self._last_foodtaken = 0 278 | self.name = self._params.Name 279 | 280 | self._odesolver = ode(self.model).set_integrator("dopri5") 281 | self._odesolver.set_initial_value(self.init_state, self.t0) 282 | 283 | self._last_action = Action(CHO=0, insulin=0) 284 | self.is_eating = False 285 | self.planned_meal = 0 286 | 287 | 288 | if __name__ == "__main__": 289 | logger.setLevel(logging.INFO) 290 | # create console handler and set level to debug 291 | ch = logging.StreamHandler() 292 | # ch.setLevel(logging.DEBUG) 293 | ch.setLevel(logging.INFO) 294 | # create formatter 295 | formatter = logging.Formatter("%(name)s: %(levelname)s: %(message)s") 296 | # add formatter to ch 297 | ch.setFormatter(formatter) 298 | # add ch to logger 299 | logger.addHandler(ch) 300 | 301 | p = T1DPatient.withName("adolescent#001") 302 | basal = p._params.u2ss * p._params.BW / 6000 # U/min 303 | t = [] 304 | CHO = [] 305 | insulin = [] 306 | BG = [] 307 | while p.t < 1000: 308 | ins = basal 309 | carb = 0 310 | if p.t == 100: 311 | carb = 80 312 | ins = 80.0 / 6.0 + basal 313 | # if p.t == 150: 314 | # ins = 80.0 / 12.0 + basal 315 | act = Action(insulin=ins, CHO=carb) 316 | t.append(p.t) 317 | CHO.append(act.CHO) 318 | insulin.append(act.insulin) 319 | BG.append(p.observation.Gsub) 320 | p.step(act) 321 | 322 | import matplotlib.pyplot as plt 323 | 324 | fig, ax = plt.subplots(3, sharex=True) 325 | ax[0].plot(t, BG) 326 | ax[1].plot(t, CHO) 327 | ax[2].plot(t, insulin) 328 | plt.show() 329 | -------------------------------------------------------------------------------- /examples/results/2017-12-31_17-46-32/risk_trace.csv: -------------------------------------------------------------------------------- 1 | ,,0,1,2,3,4,5,6,7,8 2 | LBGI,adolescent#001,0.0,0.0,0.0,0.0,0.0,0.0,1.0033973081116434,0.10163920302297062,0.0 3 | LBGI,adolescent#002,0.0,0.0,0.0,2.524850866605722,0.0,0.0,87.64704179770627,, 4 | LBGI,adolescent#003,0.0,0.0,0.0,0.0,0.0,0.0,34.53571919505026,27.767603523272935,11.99836829129833 5 | LBGI,adolescent#004,0.0,0.0,0.0,0.8857328241977845,0.0,0.0,30.241644410237623,32.34676157474706,19.363666280054186 6 | LBGI,adolescent#005,0.0,0.0,0.0,0.0,0.0,0.0,34.50362766599311,18.751356031247276,5.887652352699671 7 | LBGI,adolescent#006,0.0,0.0,0.0,0.0,0.0,0.0,0.9311218407394874,0.0,0.0 8 | LBGI,adolescent#007,0.0,0.0,0.0,0.28252277643063717,0.0,0.0,106.14089554571473,114.60600526647642,46.46055150973348 9 | LBGI,adolescent#008,0.0,0.0,0.0,0.0,0.0,0.0,45.02612556615691,11.814202933369664,0.8573679218718789 10 | LBGI,adolescent#009,0.0,0.0,0.0,1.1854202051862894,0.0,0.0,12.55978361447465,5.980631674578757,1.8586286061229544 11 | LBGI,adolescent#010,0.0,0.0,0.0,0.9953287663872002,0.0,0.0,27.115199255500592,24.82636970781543,10.232964374749045 12 | LBGI,adult#001,0.0,0.0,0.0,0.03634486450505661,0.0,0.0,1.7505630470754467,6.212836689434772,5.267751382421135 13 | LBGI,adult#002,0.0,0.0,0.0,0.0,0.0,0.0,2.4935572111771727,2.307948520147386,1.3502601783782606 14 | LBGI,adult#003,0.0,0.0,0.0,0.00039088029902781317,0.0,0.0,4.970422998213648,0.9798192943909769,0.0 15 | LBGI,adult#004,0.0,0.0,0.0,0.0,0.0,0.0,2.2503700871893453,0.5595426524336377,5.412145605883343e-05 16 | LBGI,adult#005,0.0,0.0,0.0,0.13735160218831552,0.0,0.0,4.721442957537195,4.563843134952232,1.194179668794143 17 | LBGI,adult#006,0.0,0.0,0.0,0.0,0.0,0.0,2.7671778024533404,9.309266896330735,5.924787454600113 18 | LBGI,adult#007,0.0,0.0,0.0,0.32413221661084485,0.0,0.0,6.471697502588221,4.061859114689399,1.3784412786717208 19 | LBGI,adult#008,0.0,0.0,0.0,0.38495916853447365,0.0,0.0,2.5550669889305393,2.4233412221155723,1.2169653481551492 20 | LBGI,adult#009,0.0,0.0,0.0,1.4283690994038927,0.0,0.0,17.469579069977577,52.50635991899508,46.16448571763455 21 | LBGI,adult#010,0.0,0.0,0.0,0.04868733969911717,0.0,0.0,2.9134915557472367,2.4834543252188497,0.46650267261095046 22 | LBGI,child#001,0.0,0.0,1.8298173640583366,10.561718298143072,0.0,0.0,41.416638738108375,10.400363607270355,2.754613988823897 23 | LBGI,child#002,0.0,0.0,0.0,22.671847968189045,0.0,0.0,82.24196265551993,87.11688710351696,50.76159160917407 24 | LBGI,child#003,0.0,0.0,0.0,4.355775859464949,0.0,0.0,246.3234341854313,, 25 | LBGI,child#004,0.0,0.0,1.5209619599502995,15.834698926833648,0.0,0.0,47.47797245335586,24.63379085359621,13.415016405938175 26 | LBGI,child#005,0.0,0.0,0.0,0.0,0.0,0.0,1.763385202990139,1.6072526147638588,0.8816679995771497 27 | LBGI,child#006,0.0,0.0,0.0,23.998789116988625,0.0,0.0,176.37181555793967,, 28 | LBGI,child#007,0.0,0.0,0.0,0.03937707998383979,0.0,0.0,0.8007728653666466,0.0,0.0 29 | LBGI,child#008,0.0,0.0,0.0,51.8980461374041,0.0,0.0,172.96008692692305,, 30 | LBGI,child#009,0.0,0.0,0.0,0.0,0.0,0.0,1.4563265152663694,0.0,0.0 31 | LBGI,child#010,0.0,0.0,0.0,2.98149364817069,0.0,0.0,34.07568569370393,11.192422759612889,3.2391866903577924 32 | HBGI,adolescent#001,3.3319283083352618,6.6887818710841795,4.62986479702583,0.9493063252455834,4.322393698972384,3.5354712805207704,0.0,0.0,0.1184402185006607 33 | HBGI,adolescent#002,8.413246803751415,35.652034999704284,17.47600534551028,0.0,11.761481885049879,14.646429645955978,0.0,, 34 | HBGI,adolescent#003,4.941983192308702,20.48849850086353,10.92773098449783,0.062316501354477846,9.949390995624352,9.423511532341207,0.0,0.0,0.0 35 | HBGI,adolescent#004,5.105598963719472,25.76479395710967,11.250795624297208,0.0,7.297144551992105,10.817565035920191,0.0,0.0,0.0 36 | HBGI,adolescent#005,4.20378507856427,24.01240331526822,9.677305402591053,0.00739233951654625,13.249765715448088,12.790689905851032,0.0,0.0,0.0 37 | HBGI,adolescent#006,3.6798306461009833,9.594824345535853,11.211071347691455,0.9613522875480391,14.9324975725834,2.6708369618277024,0.0,0.4055797557918116,0.8562414141860983 38 | HBGI,adolescent#007,6.422023853743148,31.966254300891276,11.032635252946125,0.0,20.472001890731654,15.04473410410432,0.0,0.0,0.0 39 | HBGI,adolescent#008,6.268069035586294,30.4772782442797,15.665422722133426,0.001898377677375193,22.992063202722974,13.04165365446093,0.0,0.0,0.0 40 | HBGI,adolescent#009,3.9964469695971667,15.106125259510584,3.4913722171373585,0.0,9.272848850103458,4.44106798303417,0.0,0.0,0.0 41 | HBGI,adolescent#010,4.012201808884861,21.357071737461958,5.20842407320408,0.0,5.190048860622603,13.968089382965516,0.0,0.0,0.0 42 | HBGI,adult#001,3.2928652390101427,13.645807938242625,9.602781304007053,0.0,3.1906382062516014,4.616460444695897,0.0,0.0,0.0 43 | HBGI,adult#002,2.2162548519363083,9.240822191582794,4.161284138489151,0.1051730886533555,4.003558934759962,4.159324868023153,0.0,0.0,0.0 44 | HBGI,adult#003,5.829773694567101,17.036596235026952,6.181227427671007,0.0,14.2246907637399,7.06887704020776,0.0,0.0,0.0051356432159491705 45 | HBGI,adult#004,6.640657329741484,19.904599265139172,8.865690303003486,0.4536968077528798,16.44387110152611,10.542860581268929,0.0,0.0,0.0 46 | HBGI,adult#005,3.6346247803028255,14.270314933749532,6.543245646844172,0.0,5.893752479427664,7.4969474449203135,0.0,0.0,0.0 47 | HBGI,adult#006,2.820025877248931,17.601525882410833,12.777119998359069,0.016178158662609927,4.500090355124557,13.603932853707095,0.0,0.0,0.0 48 | HBGI,adult#007,2.8678009963123783,12.157325116517004,3.5035064925833255,0.0,5.874835052660538,6.092053573290231,0.0,0.0,0.0 49 | HBGI,adult#008,3.4020115574908854,10.677641698011657,4.0837599122531945,0.0,1.802604474601195,2.7981766269129658,0.0,0.0,0.0 50 | HBGI,adult#009,4.136350732927754,20.81687878389762,10.82414283427984,0.0,0.3270383212889002,8.014081076231545,0.0,0.0,0.0 51 | HBGI,adult#010,6.794260699658756,20.299055248089537,9.266719744299627,0.0,11.870944428521046,8.937581659263133,0.0,0.0,0.0 52 | HBGI,child#001,9.258955321695915,25.28773958086112,0.0,0.0,19.202362731883508,0.9342903164507066,0.0,0.0,0.0 53 | HBGI,child#002,4.25872660489718,25.027611717223106,0.9725814209442447,0.0,0.11877726878257248,7.462422007953128,0.0,0.0,0.0 54 | HBGI,child#003,6.062162942197898,34.26433650912471,0.019220786031238453,0.0,13.570467161760146,2.489022196306565,0.0,, 55 | HBGI,child#004,3.567594565384822,12.07441453577821,0.0,0.0,2.3310927499852196,0.20540196417272824,0.0,0.0,0.0 56 | HBGI,child#005,1.7883587982320202,4.27362637235731,3.0025168897666727,1.932663140202475,3.363487980218547,3.992566935956805,0.0,0.0,0.0 57 | HBGI,child#006,6.450900489462187,31.831606492630634,0.1508822137285896,0.0,8.65229037246233,5.2466469910879185,0.0,, 58 | HBGI,child#007,4.056038496124277,9.175651979170595,6.380547392794419,0.0,12.274494967455505,0.29522034666188884,0.0,6.671963002175366e-05,0.19753398590767565 59 | HBGI,child#008,13.144797217081003,62.35788027668855,5.705340800696045,0.0,13.327972217802166,22.199999658533017,0.0,, 60 | HBGI,child#009,6.622025045451922,9.68047678188359,7.943122418566263,0.7951893379840045,21.12156977889125,1.452320026197669,0.0,0.013340003718868784,0.36192431920019946 61 | HBGI,child#010,6.4431623939438385,29.12259084050294,0.9705008519167518,0.0,20.451291079992345,5.21874393041569,0.0,0.0,0.0 62 | Risk Index,adolescent#001,3.3319283083352618,6.6887818710841795,4.62986479702583,0.9493063252455834,4.322393698972384,3.5354712805207704,1.0033973081116434,0.10163920302297062,0.1184402185006607 63 | Risk Index,adolescent#002,8.413246803751415,35.652034999704284,17.47600534551028,2.524850866605722,11.761481885049879,14.646429645955978,87.64704179770627,, 64 | Risk Index,adolescent#003,4.941983192308702,20.48849850086353,10.92773098449783,0.062316501354477846,9.949390995624352,9.423511532341207,34.53571919505026,27.767603523272935,11.99836829129833 65 | Risk Index,adolescent#004,5.105598963719472,25.76479395710967,11.250795624297208,0.8857328241977845,7.297144551992105,10.817565035920191,30.241644410237623,32.34676157474706,19.363666280054186 66 | Risk Index,adolescent#005,4.20378507856427,24.01240331526822,9.677305402591053,0.00739233951654625,13.249765715448088,12.790689905851032,34.50362766599311,18.751356031247276,5.887652352699671 67 | Risk Index,adolescent#006,3.6798306461009833,9.594824345535853,11.211071347691455,0.9613522875480391,14.9324975725834,2.6708369618277024,0.9311218407394874,0.4055797557918116,0.8562414141860983 68 | Risk Index,adolescent#007,6.422023853743148,31.966254300891276,11.032635252946125,0.28252277643063717,20.472001890731654,15.04473410410432,106.14089554571473,114.60600526647642,46.46055150973348 69 | Risk Index,adolescent#008,6.268069035586294,30.4772782442797,15.665422722133426,0.001898377677375193,22.992063202722974,13.04165365446093,45.02612556615691,11.814202933369664,0.8573679218718789 70 | Risk Index,adolescent#009,3.9964469695971667,15.106125259510584,3.4913722171373585,1.1854202051862894,9.272848850103458,4.44106798303417,12.55978361447465,5.980631674578757,1.8586286061229544 71 | Risk Index,adolescent#010,4.012201808884861,21.357071737461958,5.20842407320408,0.9953287663872002,5.190048860622603,13.968089382965516,27.115199255500592,24.82636970781543,10.232964374749045 72 | Risk Index,adult#001,3.2928652390101427,13.645807938242625,9.602781304007053,0.03634486450505661,3.1906382062516014,4.616460444695897,1.7505630470754467,6.212836689434772,5.267751382421135 73 | Risk Index,adult#002,2.2162548519363083,9.240822191582794,4.161284138489151,0.1051730886533555,4.003558934759962,4.159324868023153,2.4935572111771727,2.307948520147386,1.3502601783782606 74 | Risk Index,adult#003,5.829773694567101,17.036596235026952,6.181227427671007,0.00039088029902781317,14.2246907637399,7.06887704020776,4.970422998213648,0.9798192943909769,0.0051356432159491705 75 | Risk Index,adult#004,6.640657329741484,19.904599265139172,8.865690303003486,0.4536968077528798,16.44387110152611,10.542860581268929,2.2503700871893453,0.5595426524336377,5.412145605883343e-05 76 | Risk Index,adult#005,3.6346247803028255,14.270314933749532,6.543245646844172,0.13735160218831552,5.893752479427664,7.4969474449203135,4.721442957537195,4.563843134952232,1.194179668794143 77 | Risk Index,adult#006,2.820025877248931,17.601525882410833,12.777119998359069,0.016178158662609927,4.500090355124557,13.603932853707095,2.7671778024533404,9.309266896330735,5.924787454600113 78 | Risk Index,adult#007,2.8678009963123783,12.157325116517004,3.5035064925833255,0.32413221661084485,5.874835052660538,6.092053573290231,6.471697502588221,4.061859114689399,1.3784412786717208 79 | Risk Index,adult#008,3.4020115574908854,10.677641698011657,4.0837599122531945,0.38495916853447365,1.802604474601195,2.7981766269129658,2.5550669889305393,2.4233412221155723,1.2169653481551492 80 | Risk Index,adult#009,4.136350732927754,20.81687878389762,10.82414283427984,1.4283690994038927,0.3270383212889002,8.014081076231545,17.469579069977577,52.50635991899508,46.16448571763455 81 | Risk Index,adult#010,6.794260699658756,20.299055248089537,9.266719744299627,0.04868733969911717,11.870944428521046,8.937581659263133,2.9134915557472367,2.4834543252188497,0.46650267261095046 82 | Risk Index,child#001,9.258955321695915,25.28773958086112,1.8298173640583366,10.561718298143072,19.202362731883508,0.9342903164507066,41.416638738108375,10.400363607270355,2.754613988823897 83 | Risk Index,child#002,4.25872660489718,25.027611717223106,0.9725814209442447,22.671847968189045,0.11877726878257248,7.462422007953128,82.24196265551993,87.11688710351696,50.76159160917407 84 | Risk Index,child#003,6.062162942197898,34.26433650912471,0.019220786031238453,4.355775859464949,13.570467161760146,2.489022196306565,246.3234341854313,, 85 | Risk Index,child#004,3.567594565384822,12.07441453577821,1.5209619599502995,15.834698926833648,2.3310927499852196,0.20540196417272824,47.47797245335586,24.63379085359621,13.415016405938175 86 | Risk Index,child#005,1.7883587982320202,4.27362637235731,3.0025168897666727,1.932663140202475,3.363487980218547,3.992566935956805,1.763385202990139,1.6072526147638588,0.8816679995771497 87 | Risk Index,child#006,6.450900489462187,31.831606492630634,0.1508822137285896,23.998789116988625,8.65229037246233,5.2466469910879185,176.37181555793967,, 88 | Risk Index,child#007,4.056038496124277,9.175651979170595,6.380547392794419,0.03937707998383979,12.274494967455505,0.29522034666188884,0.8007728653666466,6.671963002175366e-05,0.19753398590767565 89 | Risk Index,child#008,13.144797217081003,62.35788027668855,5.705340800696045,51.8980461374041,13.327972217802166,22.199999658533017,172.96008692692305,, 90 | Risk Index,child#009,6.622025045451922,9.68047678188359,7.943122418566263,0.7951893379840045,21.12156977889125,1.452320026197669,1.4563265152663694,0.013340003718868784,0.36192431920019946 91 | Risk Index,child#010,6.4431623939438385,29.12259084050294,0.9705008519167518,2.98149364817069,20.451291079992345,5.21874393041569,34.07568569370393,11.192422759612889,3.2391866903577924 92 | -------------------------------------------------------------------------------- /simglucose/analysis/report.py: -------------------------------------------------------------------------------- 1 | import glob 2 | import pandas as pd 3 | import numpy as np 4 | import os 5 | import matplotlib.pyplot as plt 6 | import matplotlib.dates as mdates 7 | from matplotlib.collections import PatchCollection 8 | # from pandas.plotting import lag_plot 9 | import logging 10 | 11 | logger = logging.getLogger(__name__) 12 | 13 | 14 | def ensemble_BG(BG, ax=None, plot_var=False, nstd=3): 15 | mean_curve = BG.transpose().mean() 16 | std_curve = BG.transpose().std() 17 | up_env = mean_curve + nstd * std_curve 18 | down_env = mean_curve - nstd * std_curve 19 | 20 | # t = BG.index.to_pydatetime() 21 | t = pd.to_datetime(BG.index) 22 | if ax is None: 23 | fig, ax = plt.subplots(1) 24 | if plot_var and not std_curve.isnull().all(): 25 | ax.fill_between( 26 | t, up_env, down_env, alpha=0.5, label='+/- {0}*std'.format(nstd)) 27 | for p in BG: 28 | ax.plot_date( 29 | t, BG[p], '-', color='grey', alpha=0.5, lw=0.5, label='_nolegend_') 30 | ax.plot(t, mean_curve, lw=2, label='Mean Curve') 31 | ax.xaxis.set_minor_locator(mdates.HourLocator(interval=3)) 32 | ax.xaxis.set_minor_formatter(mdates.DateFormatter('%H:%M\n')) 33 | ax.xaxis.set_major_locator(mdates.DayLocator()) 34 | ax.xaxis.set_major_formatter(mdates.DateFormatter('\n%b %d')) 35 | 36 | ax.axhline(70, c='green', linestyle='--', label='Hypoglycemia', lw=1) 37 | ax.axhline(180, c='red', linestyle='--', label='Hyperglycemia', lw=1) 38 | 39 | ax.set_xlim([t[0], t[-1]]) 40 | ax.set_ylim([BG.min().min() - 10, BG.max().max() + 10]) 41 | ax.legend() 42 | ax.set_ylabel('Blood Glucose (mg/dl)') 43 | # fig.autofmt_xdate() 44 | return ax 45 | 46 | 47 | def ensemblePlot(df): 48 | df_BG = df.unstack(level=0).BG 49 | df_CGM = df.unstack(level=0).CGM 50 | df_CHO = df.unstack(level=0).CHO 51 | fig = plt.figure() 52 | ax1 = fig.add_subplot(311) 53 | ax2 = fig.add_subplot(312) 54 | ax3 = fig.add_subplot(313) 55 | ax1 = ensemble_BG(df_BG, ax=ax1, plot_var=True, nstd=1) 56 | ax2 = ensemble_BG(df_CGM, ax=ax2, plot_var=True, nstd=1) 57 | # t = df_CHO.index.to_pydatetime() 58 | t = pd.to_datetime(df_CHO.index) 59 | ax3.plot(t, df_CHO) 60 | 61 | ax1.tick_params(labelbottom=False) 62 | ax2.tick_params(labelbottom=False) 63 | ax3.xaxis.set_minor_locator(mdates.AutoDateLocator()) 64 | ax3.xaxis.set_minor_formatter(mdates.DateFormatter('%H:%M\n')) 65 | ax3.xaxis.set_major_locator(mdates.DayLocator()) 66 | ax3.xaxis.set_major_formatter(mdates.DateFormatter('\n%b %d')) 67 | ax3.set_xlim([t[0], t[-1]]) 68 | ax1.set_ylabel('Blood Glucose (mg/dl)') 69 | ax2.set_ylabel('CGM (mg/dl)') 70 | ax3.set_ylabel('CHO (g)') 71 | return fig, ax1, ax2, ax3 72 | 73 | 74 | def percent_stats(BG, ax=None): 75 | if ax is None: 76 | fig, ax = plt.subplots(1) 77 | p_hyper = (BG > 180).sum() / len(BG) * 100 78 | p_hyper.name = 'BG>180' 79 | p_hypo = (BG < 70).sum() / len(BG) * 100 80 | p_hypo.name = 'BG<70' 81 | p_normal = ((BG >= 70) & (BG <= 180)).sum() / len(BG) * 100 82 | p_normal.name = '70<=BG<=180' 83 | p_250 = (BG > 250).sum() / len(BG) * 100 84 | p_250.name = 'BG>250' 85 | p_50 = (BG < 50).sum() / len(BG) * 100 86 | p_50.name = 'BG<50' 87 | p_stats = pd.concat([p_normal, p_hyper, p_hypo, p_250, p_50], axis=1) 88 | p_stats.plot(ax=ax, kind='bar') 89 | ax.set_ylabel('Percent of time in Range (%)') 90 | fig.tight_layout() 91 | # p_stats.transpose().plot(kind='bar', legend=False) 92 | return p_stats, fig, ax 93 | 94 | 95 | def risk_index_trace(df_BG, sample_time=3, window_length=60, visualize=False): 96 | step_size = int(window_length / sample_time) # window size set to 1 hour for calculating Risk Index 97 | chunk_BG = [df_BG.iloc[i:i + step_size, :] for i in range(0, len(df_BG), step_size)] 98 | 99 | if len(chunk_BG[-1]) != step_size: # Remove the last chunk which is not full 100 | chunk_BG.pop() 101 | 102 | fBG = [ 103 | 1.509 * (np.log(BG[BG > 0]) ** 1.084 - 5.381) for BG in chunk_BG 104 | ] 105 | 106 | rl = [(10 * (fbg * (fbg < 0)) ** 2).mean() for fbg in fBG] 107 | rh = [(10 * (fbg * (fbg > 0)) ** 2).mean() for fbg in fBG] 108 | 109 | LBGI = pd.concat(rl, axis=1).transpose() 110 | HBGI = pd.concat(rh, axis=1).transpose() 111 | RI = LBGI + HBGI 112 | 113 | ri_per_hour = pd.concat( 114 | [LBGI.transpose(), HBGI.transpose(), 115 | RI.transpose()], 116 | keys=['LBGI', 'HBGI', 'Risk Index']) 117 | 118 | axes = [] 119 | if visualize: 120 | logger.info('Plotting risk trace plot') 121 | ri_per_hour_plot = pd.concat( 122 | [HBGI.transpose(), -LBGI.transpose()], keys=['HBGI', '-LBGI']) 123 | for i in range(len(ri_per_hour_plot.unstack(level=0))): 124 | logger.debug( 125 | ri_per_hour_plot.unstack(level=0).iloc[i].unstack(level=1)) 126 | axtmp = ri_per_hour_plot.unstack(level=0).iloc[i].unstack( 127 | level=1).plot.bar(stacked=True) 128 | axes.append(axtmp) 129 | plt.xlabel('Time (hour)') 130 | plt.ylabel('Risk Index') 131 | 132 | ri_mean = ri_per_hour.transpose().mean().unstack(level=0) 133 | fig, ax = plt.subplots(1) 134 | ri_mean.plot(ax=ax, kind='bar') 135 | fig.tight_layout() 136 | 137 | axes.append(ax) 138 | return ri_per_hour, ri_mean, fig, axes 139 | 140 | 141 | def CVGA_background(ax=None): 142 | if ax is None: 143 | fig, ax = plt.subplots(1) 144 | 145 | ax.set_xlim(109, 49) 146 | ax.set_ylim(105, 405) 147 | ax.set_xticks([110, 90, 70, 50]) 148 | ax.set_yticks([110, 180, 300, 400]) 149 | ax.set_xticklabels(['110', '90', '70', '<50']) 150 | ax.set_yticklabels(['110', '180', '300', '>400']) 151 | # fig.suptitle('Control Variability Grid Analysis (CVGA)') 152 | ax.set_title('Control Variability Grid Analysis (CVGA)') 153 | ax.set_xlabel('Min BG (2.5th percentile)') 154 | ax.set_ylabel('Max BG (97.5th percentile)') 155 | ax.spines['top'].set_visible(False) 156 | ax.spines['right'].set_visible(False) 157 | ax.spines['bottom'].set_visible(False) 158 | ax.spines['left'].set_visible(False) 159 | 160 | rectangles = { 161 | 'A-Zone': plt.Rectangle((90, 110), 20, 70, color='limegreen'), 162 | 'Lower B': plt.Rectangle((70, 110), 20, 70, color='green'), 163 | 'Upper B': plt.Rectangle((90, 180), 20, 120, color='green'), 164 | 'B-Zone': plt.Rectangle((70, 180), 20, 120, color='green'), 165 | 'Lower C': plt.Rectangle((50, 110), 20, 70, color='yellow'), 166 | 'Upper C': plt.Rectangle((90, 300), 20, 100, color='yellow'), 167 | 'Lower D': plt.Rectangle((50, 180), 20, 120, color='orange'), 168 | 'Upper D': plt.Rectangle((70, 300), 20, 100, color='orange'), 169 | 'E-Zone': plt.Rectangle((50, 300), 20, 100, color='red') 170 | } 171 | facecolors = [rectangles[r].get_facecolor() for r in rectangles] 172 | pc = PatchCollection( 173 | rectangles.values(), 174 | facecolor=facecolors, 175 | edgecolors='w', 176 | lw=2, 177 | alpha=1) 178 | ax.add_collection(pc) 179 | for r in rectangles: 180 | rx, ry = rectangles[r].get_xy() 181 | cx = rx + rectangles[r].get_width() / 2.0 182 | cy = ry + rectangles[r].get_height() / 2.0 183 | if r in ['Lower B', 'Upper B', 'B-Zone']: 184 | ax.annotate( 185 | r, (cx, cy), 186 | weight='bold', 187 | color='w', 188 | fontsize=10, 189 | ha='center', 190 | va='center') 191 | else: 192 | ax.annotate( 193 | r, (cx, cy), 194 | weight='bold', 195 | color='k', 196 | fontsize=10, 197 | ha='center', 198 | va='center') 199 | 200 | return fig, ax 201 | 202 | 203 | def CVGA_analysis(BG): 204 | BG_min = np.percentile(BG, 2.5, axis=0) 205 | BG_max = np.percentile(BG, 97.5, axis=0) 206 | BG_min[BG_min < 50] = 50 207 | BG_min[BG_min > 400] = 400 208 | BG_max[BG_max < 50] = 50 209 | BG_max[BG_max > 400] = 400 210 | 211 | perA = ((BG_min > 90) & (BG_min <= 110) & (BG_max >= 110) 212 | & (BG_max < 180)).sum() / float(len(BG_min)) 213 | perB = ((BG_min > 70) & (BG_min <= 110) & (BG_max >= 110) 214 | & (BG_max < 300)).sum() / float(len(BG_min)) - perA 215 | perC = (((BG_min > 90) & (BG_min <= 110) & (BG_max >= 300)) | 216 | ((BG_min <= 70) & (BG_max >= 110) & 217 | (BG_max < 180))).sum() / float(len(BG_min)) 218 | perD = (((BG_min > 70) & (BG_min <= 90) & (BG_max >= 300)) | 219 | ((BG_min <= 70) & (BG_max >= 180) & 220 | (BG_max < 300))).sum() / float(len(BG_min)) 221 | perE = ((BG_min <= 70) & (BG_max >= 300)).sum() / float(len(BG_min)) 222 | return BG_min, BG_max, perA, perB, perC, perD, perE 223 | 224 | 225 | def CVGA(BG_list, label=None): 226 | if not isinstance(BG_list, list): 227 | BG_list = [BG_list] 228 | if not isinstance(label, list): 229 | label = [label] 230 | if label is None: 231 | label = ['BG%d' % (i + 1) for i in range(len(BG_list))] 232 | fig, ax = CVGA_background() 233 | zone_stats = [] 234 | for (BG, l) in zip(BG_list, label): 235 | BGmin, BGmax, A, B, C, D, E = CVGA_analysis(BG) 236 | ax.scatter( 237 | BGmin, 238 | BGmax, 239 | edgecolors='k', 240 | zorder=4, 241 | label='%s (A: %d%%, B: %d%%, C: %d%%, D: %d%%, E: %d%%)' % 242 | (l, 100 * A, 100 * B, 100 * C, 100 * D, 100 * E)) 243 | zone_stats.append((A, B, C, D, E)) 244 | 245 | zone_stats = pd.DataFrame(zone_stats, columns=['A', 'B', 'C', 'D', 'E']) 246 | # ax.legend(bbox_to_anchor=(1, 1.10), borderaxespad=0.5) 247 | ax.legend() 248 | return zone_stats, fig, ax 249 | 250 | 251 | def report(df, cgm_sensor=None, save_path=None): 252 | BG = df.unstack(level=0).BG 253 | 254 | fig_ensemble, ax1, ax2, ax3 = ensemblePlot(df) 255 | pstats, fig_percent, ax4 = percent_stats(BG) 256 | if cgm_sensor is not None: 257 | ri_per_hour, ri_mean, fig_ri, ax5 = risk_index_trace(BG, sample_time=cgm_sensor.sample_time, visualize=False) 258 | else: 259 | ri_per_hour, ri_mean, fig_ri, ax5 = risk_index_trace(BG, visualize=False) 260 | zone_stats, fig_cvga, ax6 = CVGA(BG, label='') 261 | axes = [ax1, ax2, ax3, ax4, ax5, ax6] 262 | figs = [fig_ensemble, fig_percent, fig_ri, fig_cvga] 263 | results = pd.concat([pstats, ri_mean], axis=1) 264 | 265 | if save_path is not None: 266 | results.to_csv(os.path.join(save_path, 'performance_stats.csv')) 267 | ri_per_hour.to_csv(os.path.join(save_path, 'risk_trace.csv')) 268 | zone_stats.to_csv(os.path.join(save_path, 'CVGA_stats.csv')) 269 | 270 | fig_ensemble.savefig(os.path.join(save_path, 'BG_trace.png')) 271 | fig_percent.savefig(os.path.join(save_path, 'zone_stats.png')) 272 | fig_ri.savefig(os.path.join(save_path, 'risk_stats.png')) 273 | fig_cvga.savefig(os.path.join(save_path, 'CVGA.png')) 274 | 275 | plt.show() 276 | return results, ri_per_hour, zone_stats, figs, axes 277 | 278 | 279 | if __name__ == '__main__': 280 | logger.setLevel(logging.DEBUG) 281 | # create file handler which logs even debug messages 282 | fh = logging.FileHandler('analysis.log') 283 | fh.setLevel(logging.DEBUG) 284 | # create console handler with a higher log level 285 | ch = logging.StreamHandler() 286 | ch.setLevel(logging.DEBUG) 287 | # create formatter and add it to the handlers 288 | formatter = logging.Formatter( 289 | '%(asctime)s - %(name)s - %(levelname)s - \n %(message)s') 290 | fh.setFormatter(formatter) 291 | ch.setFormatter(formatter) 292 | 293 | # add the handlers to the logger 294 | # logger.addHandler(fh) 295 | logger.addHandler(ch) 296 | # For test only 297 | path = os.path.join('..', '..', 'examples', 'results', 298 | '2017-12-31_17-46-32') 299 | os.chdir(path) 300 | filename = glob.glob('*#*.csv') 301 | name = [_f[:-4] for _f in filename] 302 | df = pd.concat([pd.read_csv(f, index_col=0) for f in filename], keys=name) 303 | # df_BG = df.unstack(level=0).BG 304 | # df_CGM = df.unstack(level=0).CGM 305 | # report(df_BG, df_CGM) 306 | results, ri_per_hour, zone_stats, axes = report(df) 307 | # print results 308 | # # print ri_per_hour 309 | # print zone_stats 310 | -------------------------------------------------------------------------------- /simglucose/simulation/user_interface.py: -------------------------------------------------------------------------------- 1 | from simglucose.simulation.sim_engine import SimObj, batch_sim 2 | from simglucose.simulation.env import T1DSimEnv 3 | from simglucose.controller.basal_bolus_ctrller import BBController 4 | from simglucose.sensor.cgm import CGMSensor 5 | from simglucose.actuator.pump import InsulinPump 6 | from simglucose.patient.t1dpatient import T1DPatient 7 | from simglucose.simulation.scenario_gen import RandomScenario 8 | from simglucose.simulation.scenario import CustomScenario 9 | from simglucose.analysis.report import report 10 | import pandas as pd 11 | import copy 12 | import pkg_resources 13 | import logging 14 | import os 15 | from datetime import datetime 16 | from datetime import timedelta 17 | import platform 18 | 19 | logger = logging.getLogger(__name__) 20 | 21 | PATIENT_PARA_FILE = pkg_resources.resource_filename( 22 | "simglucose", "params/vpatient_params.csv" 23 | ) 24 | SENSOR_PARA_FILE = pkg_resources.resource_filename( 25 | "simglucose", "params/sensor_params.csv" 26 | ) 27 | INSULIN_PUMP_PARA_FILE = pkg_resources.resource_filename( 28 | "simglucose", "params/pump_params.csv" 29 | ) 30 | 31 | 32 | def pick_patients(): 33 | patient_params = pd.read_csv(PATIENT_PARA_FILE) 34 | patient_names = list(patient_params["Name"].values) 35 | while True: 36 | select1 = input( 37 | "Select virtual patients:\n" 38 | + "[1] All\n" 39 | + "[2] All Adolescents\n" 40 | + "[3] All Adults\n" 41 | + "[4] All Children\n" 42 | + "[5] By ID\n" 43 | + ">>> " 44 | ) 45 | try: 46 | select1 = int(select1) 47 | except ValueError: 48 | print("Please input an integer. Try again") 49 | input("Press any key to continue ...") 50 | continue 51 | 52 | if select1 < 1 or select1 > 5: 53 | print("Input 1 to 5 please!") 54 | input("Press any key to continue ...") 55 | continue 56 | else: 57 | break 58 | 59 | if select1 == 1: 60 | patients = patient_names 61 | elif select1 == 2: 62 | patients = patient_names[:10] 63 | elif select1 == 3: 64 | patients = patient_names[10:20] 65 | elif select1 == 4: 66 | patients = patient_names[20:30] 67 | else: 68 | patients = [] 69 | select_hist = [] 70 | while True: 71 | print("Select patient:") 72 | for i, p in enumerate(patient_names): 73 | print("[{0}] {1}".format(i + 1, p)) 74 | print("[D] Done") 75 | select2 = input(">>> ") 76 | 77 | if select2 == "D" or select2 == "d": 78 | break 79 | 80 | try: 81 | select2 = int(select2) 82 | except ValueError: 83 | print("Please input a number or 'D' or 'd'.") 84 | input("Press any key to continue ...") 85 | continue 86 | 87 | if select2 < 1 or select2 > 30: 88 | print( 89 | "Please input an number from 1 to {0}.".format(len(patient_names)) 90 | ) 91 | input("Press any key to continue ...") 92 | continue 93 | 94 | if select2 in select_hist: 95 | print("{0} is already selected!".format(patient_names[select2 - 1])) 96 | input("Press any key to continue ...") 97 | continue 98 | else: 99 | select_hist.append(select2) 100 | patients.append(patient_names[select2 - 1]) 101 | logger.info("Selected patients:\n{}".format(patients)) 102 | return patients 103 | 104 | 105 | def pick_cgm_sensor(): 106 | sensor_params = pd.read_csv(SENSOR_PARA_FILE) 107 | sensor_names = list(sensor_params["Name"].values) 108 | total_sensor_num = len(sensor_params.index) 109 | while True: 110 | print("Select the CGM sensor:") 111 | for i in range(total_sensor_num): 112 | print("[{0}] {1}".format(i + 1, sensor_names[i])) 113 | input_value = input(">>> ") 114 | try: 115 | selection = int(input_value) 116 | except ValueError: 117 | print("Oops! Please input a number.") 118 | input("Press any key to continue ...") 119 | continue 120 | if selection < 1 or selection > total_sensor_num: 121 | print("Please input an integer from 1 to {0}!".format(total_sensor_num)) 122 | input("Press any key to continue ...") 123 | continue 124 | else: 125 | break 126 | sensor = sensor_names[selection - 1] 127 | logger.info("Selected sensor:\n{}".format(sensor)) 128 | return sensor 129 | 130 | 131 | def pick_cgm_seed(): 132 | while True: 133 | input_value = input("Select Random Seed for Sensor Noise [None]: ") 134 | try: 135 | seed = int(input_value) 136 | break 137 | except ValueError: 138 | if input_value == "" or input_value == "None": 139 | seed = None 140 | break 141 | else: 142 | print("Please input an integer!") 143 | continue 144 | logger.info("Sensor Random Seed: {}".format(seed)) 145 | return seed 146 | 147 | 148 | def pick_insulin_pump(): 149 | pump_params = pd.read_csv(INSULIN_PUMP_PARA_FILE) 150 | pump_names = list(pump_params["Name"].values) 151 | while True: 152 | print("Select the insulin pump:") 153 | for i, pump in enumerate(pump_names): 154 | print("[{}] {}".format(i + 1, pump)) 155 | input_value = input(">>> ") 156 | try: 157 | selection = int(input_value) 158 | except ValueError: 159 | print("Oops! Please input a number.") 160 | input("Press any key to continue ...") 161 | continue 162 | if selection < 1 or selection > len(pump_names): 163 | print("Please input an integer from 1 to {0}!".format(len(pump_names))) 164 | input("Press any key to continue ...") 165 | continue 166 | break 167 | pump = pump_names[selection - 1] 168 | logger.info("Selected Pumps:\n{}".format(pump)) 169 | return pump 170 | 171 | 172 | def pick_scenario(start_time=None): 173 | while True: 174 | print("Select scnenario:") 175 | print("[1] Random Scnenario") 176 | print("[2] Custom Scnenario") 177 | input_value = input(">>>") 178 | try: 179 | selection = int(input_value) 180 | except ValueError: 181 | print("Please input an integer!") 182 | continue 183 | if selection < 1 or selection > 2: 184 | print("Please input a number from the list!") 185 | else: 186 | break 187 | 188 | if start_time is None: 189 | start_time = pick_start_time() 190 | 191 | if selection == 1: 192 | while True: 193 | input_value = input("Select random seed for random scenario [None]: ") 194 | try: 195 | seed = int(input_value) 196 | break 197 | except ValueError: 198 | if input_value in ("", "None"): 199 | seed = None 200 | break 201 | print("Please input an integer!") 202 | continue 203 | scenario = RandomScenario(start_time, seed=seed) 204 | else: 205 | custom_scenario = input_custom_scenario() 206 | scenario = CustomScenario(start_time, custom_scenario) 207 | 208 | return scenario 209 | 210 | 211 | def pick_start_time(): 212 | now = datetime.now() 213 | start_hour = timedelta(hours=float(input("Input simulation start time (hr): "))) 214 | start_time = datetime.combine(now.date(), datetime.min.time()) + start_hour 215 | print("Simulation start time is set to {}.".format(start_time)) 216 | return start_time 217 | 218 | 219 | def input_custom_scenario(): 220 | scenario = [] 221 | 222 | print("Input a custom scenario ...") 223 | breakfast_time = float(input("Input breakfast time (hr): ")) 224 | breakfast_size = float(input("Input breakfast size (g): ")) 225 | scenario.append((breakfast_time, breakfast_size)) 226 | 227 | lunch_time = float(input("Input lunch time (hr): ")) 228 | lunch_size = float(input("Input lunch size (g): ")) 229 | scenario.append((lunch_time, lunch_size)) 230 | 231 | dinner_time = float(input("Input dinner time (hr): ")) 232 | dinner_size = float(input("Input dinner size (g): ")) 233 | scenario.append((dinner_time, dinner_size)) 234 | 235 | while True: 236 | snack_time = float(input("Input snack time (hr): ")) 237 | snack_size = float(input("Input snack size (g): ")) 238 | scenario.append((snack_time, snack_size)) 239 | 240 | go_on = input("Continue input snack (y/n)? ") 241 | if go_on == "n": 242 | break 243 | elif go_on == "y": 244 | continue 245 | else: 246 | go_on = input("Continue input snack (y/n)? ") 247 | return scenario 248 | 249 | 250 | def pick_controller(): 251 | controller = None 252 | while True: 253 | print("Select controller:") 254 | print("[1] Basal-Bolus Controller") 255 | input_value = input(">>>") 256 | try: 257 | selection = int(input_value) 258 | except ValueError: 259 | print("Please input an integer!") 260 | continue 261 | if selection < 1 or selection > 1: 262 | print("Please input a number from the list!") 263 | else: 264 | break 265 | if selection == 1: 266 | controller = BBController() 267 | return controller 268 | 269 | 270 | def pick_save_path(use_default=False): 271 | if not use_default: 272 | foldername = input("Folder name to save results [default]: ") 273 | if foldername == "default" or foldername == "": 274 | foldername = datetime.now().strftime("%Y-%m-%d_%H-%M-%S") 275 | else: 276 | foldername = datetime.now().strftime("%Y-%m-%d_%H-%M-%S") 277 | 278 | save_path = os.path.join(os.path.abspath("./results/"), foldername) 279 | print("Results will be saved in {}".format(save_path)) 280 | return save_path 281 | 282 | 283 | def pick_animate(): 284 | while True: 285 | select = input("Show animation? (y/n) ") 286 | if select == "y": 287 | animate = True 288 | break 289 | elif select == "n": 290 | animate = False 291 | break 292 | else: 293 | continue 294 | return animate 295 | 296 | 297 | def pick_parallel(): 298 | while True: 299 | select = input("Use multiple processes? (y/n) ") 300 | if select == "y": 301 | parallel = True 302 | break 303 | elif select == "n": 304 | parallel = False 305 | break 306 | else: 307 | continue 308 | return parallel 309 | 310 | 311 | def simulate( 312 | sim_time=None, 313 | scenario=None, 314 | controller=None, 315 | patient_names=[], 316 | cgm_name=None, 317 | cgm_seed=None, 318 | insulin_pump_name=None, 319 | start_time=None, 320 | save_path=None, 321 | animate=None, 322 | parallel=None, 323 | ): 324 | """ 325 | Main user interface. 326 | ---- 327 | Inputs: 328 | sim_time - a datetime.timedelta object specifying the simulation time. 329 | scenario - a simglucose.scenario.Scenario object. Use 330 | simglucose.scenario_gen.RandomScenario or 331 | simglucose.scenario.CustomScenario to create a scenario object. 332 | controller - a simglucose.controller.Controller object. 333 | start_time - a datetime.datetime object specifying the simulation start time. 334 | save_path - a string representing the directory to save simulation results. 335 | animate - switch for animation. True/False. 336 | parallel - switch for parallel computing. True/False. 337 | """ 338 | if animate is None: 339 | animate = pick_animate() 340 | 341 | if parallel is None: 342 | parallel = pick_parallel() 343 | 344 | if platform.system() == "Darwin" and (animate and parallel): 345 | raise ValueError( 346 | """animate and parallel cannot be turned on at the same time in macOS.""" 347 | ) 348 | 349 | if save_path is None: 350 | save_path = pick_save_path() 351 | elif save_path == "default": 352 | save_path = pick_save_path(use_default=True) 353 | elif save_path == "None": 354 | save_path = None 355 | 356 | if sim_time is None: 357 | sim_time = timedelta(hours=float(input("Input simulation time (hr): "))) 358 | 359 | if scenario is None: 360 | scenario = pick_scenario(start_time=start_time) 361 | 362 | if not patient_names: 363 | patient_names = pick_patients() 364 | 365 | if cgm_name is None: 366 | cgm_name = pick_cgm_sensor() 367 | 368 | if cgm_seed is None: 369 | cgm_seed = pick_cgm_seed() 370 | 371 | if insulin_pump_name is None: 372 | insulin_pump_name = pick_insulin_pump() 373 | 374 | if controller is None: 375 | controller = pick_controller() 376 | 377 | cgm_sensor = CGMSensor.withName(cgm_name, seed=cgm_seed) 378 | 379 | def local_build_env(pname): 380 | patient = T1DPatient.withName(pname) 381 | insulin_pump = InsulinPump.withName(insulin_pump_name) 382 | scen = copy.deepcopy(scenario) 383 | env = T1DSimEnv(patient, cgm_sensor, insulin_pump, scen) 384 | return env 385 | 386 | envs = [local_build_env(p) for p in patient_names] 387 | 388 | ctrllers = [copy.deepcopy(controller) for _ in range(len(envs))] 389 | sim_instances = [ 390 | SimObj(e, c, sim_time, animate=animate, path=save_path) 391 | for (e, c) in zip(envs, ctrllers) 392 | ] 393 | 394 | results = batch_sim(sim_instances, parallel=parallel) 395 | 396 | df = pd.concat(results, keys=[s.env.patient.name for s in sim_instances]) 397 | results, ri_per_hour, zone_stats, figs, axes = report(df, cgm_sensor, save_path) 398 | 399 | return results 400 | 401 | 402 | if __name__ == "__main__": 403 | root = logging.getLogger() 404 | root.setLevel(logging.DEBUG) 405 | # logger.setLevel(logging.INFO) 406 | # create console handler and set level to debug 407 | ch = logging.StreamHandler() 408 | # ch.setLevel(logging.DEBUG) 409 | ch.setLevel(logging.INFO) 410 | # create formatter 411 | formatter = logging.Formatter("%(process)d: %(name)s: %(levelname)s: %(message)s") 412 | # add formatter to ch 413 | ch.setFormatter(formatter) 414 | # add ch to logger 415 | root.addHandler(ch) 416 | 417 | simulate() 418 | -------------------------------------------------------------------------------- /simglucose/params/vpatient_params.csv: -------------------------------------------------------------------------------- 1 | Name,i,x0_ 1,x0_ 2,x0_ 3,x0_ 4,x0_ 5,x0_ 6,x0_ 7,x0_ 8,x0_ 9,x0_10,x0_11,x0_12,x0_13,BW,EGPb,Gb,Ib,kabs,kmax,kmin,b,d,Vg,Vi,Ipb,Vmx,Km0,k2,k1,p2u,m1,m5,CL,HEb,m2,m4,m30,Ilb,ki,kp2,kp3,f,Gpb,ke1,ke2,Fsnc,Gtb,Vm0,Rdb,PCRb,kd,ksc,ka1,ka2,dosekempt,u2ss,isc1ss,isc2ss,kp1,patient_history 2 | adolescent#001,1,0,0,0,250.621836,176.506559902,4.697517762,0,97.554,97.554,3.19814917262,57.951224472,93.2258828462,250.621836,68.706,3.3924,149.02,97.554,0.091043,0.015865,0.0083338,0.83072,0.32294,1.6818,0.048153,4.697517762,0.074667,260.89,0.067738,0.057252,0.021344,0.15221,0.029902,0.8571,0.6,0.259067825939,0.103627130376,0.228315,3.19814917262,0.0088838,0.023318,0.023253,0.9,250.621836,0.0005,339,1,176.506559902,5.92854753098,3.3924,0.0227647295665,0.0185,0.056,0.0025,0.0115,90000,1.21697571391,57.951224472,93.2258828462,11.5048231338,0 3 | adolescent#002,2,0,0,0,280.236267,30.9541309204,6.01930508,0,119.18,119.18,4.96870842374,77.8484760287,47.1721248798,280.236267,51.046,2.4905,152.41,119.18,0.12473,0.034573,0.006983,0.76876,0.18887,1.8387,0.050506,6.01930508,0.038897,223.01,0.28643,0.036957,0.017681,0.14477,0.036533,0.77023,0.6,0.298755383281,0.119502153313,0.217155,4.96870842374,0.0084603,0.0026879,0.0079949,0.9,280.236267,0.0005,339,1,30.9541309204,12.228853658,2.4905,0.0163407912867,0.0163,0.1314,0.0068,0.0269,90000,1.79829979626,77.8484760287,47.1721248798,4.19657924407,0 4 | adolescent#003,3,0,0,0,326.55338,383.368339011,3.48788064,0,101.28,101.28,5.27738007224,80.7969837316,81.3429092974,326.55338,44.791,2.8199,145.9,101.28,0.046752,0.046773,0.0066262,0.56424,0.14423,2.2382,0.034438,3.48788064,0.060762,216.57,0.080467,0.10004,0.065784,0.10962,0.016546,0.63961,0.6,0.414654673732,0.165861869493,0.16443,5.27738007224,0.0094229,0.0060918,0.017571,0.9,326.55338,0.0005,339,1,383.368339011,2.84798631515,2.8199,0.0193276216587,0.0149,0.1059,0.003,0.0148,90000,1.4462660088,80.7969837316,81.3429092974,6.58878876028,0 5 | adolescent#004,4,0,0,0,248.11709,214.758607646,4.31676536,0,108.88,108.88,3.95608314762,97.383030189,124.746856854,248.11709,49.564,3.2437,144.7,108.88,0.14452,0.030833,0.0096077,0.77607,0.14467,1.7147,0.039647,4.31676536,0.056525,248.78,0.07298,0.072211,0.037027,0.17822,0.02182,0.80238,0.6,0.408322597923,0.163329039169,0.26733,3.95608314762,0.0058346,0.0046256,0.015157,0.9,248.11709,0.0005,339,1,214.758607646,4.84283999312,3.2437,0.0224167242571,0.0155,0.0751,0.0026,0.0121,90000,1.76263284642,97.383030189,124.746856854,6.0416845715,0 6 | adolescent#005,5,0,0,0,279.047113,144.715758769,5.16289777,0,113.09,113.09,4.63464033793,83.8603979178,71.5419213875,279.047113,47.074,2.8283,139.03,113.09,1.2323,0.025659,0.025281,0.98829,0.13898,2.0071,0.045653,5.16289777,0.050384,278.74,0.14022,0.079271,0.032703,0.13245,0.014914,0.6388,0.6,0.297244948528,0.118897979411,0.198675,4.63464033793,0.010166,0.0039814,0.0062157,0.9,279.047113,0.0005,339,1,144.715758769,5.34982624105,2.8283,0.0203430914191,0.0151,0.0866,0.0032,0.0177,90000,1.5346452819,83.8603979178,71.5419213875,4.6422316887,0 7 | adolescent#006,6,0,0,0,257.421705,162.558570565,8.154419,0,113.0,113.0,1.7150416755,79.9949521754,52.7439245113,257.421705,45.408,3.792,134.67,113.0,0.044109,0.058757,0.0075623,0.62856,0.17249,1.9115,0.072163,8.154419,0.043789,205.67,0.084141,0.06398,0.046778,0.44964,0.021571,0.7747,0.6,0.236421300822,0.0945685203288,0.67446,1.7150416755,0.044238,0.012289,0.0062812,0.9,257.421705,0.0005,339,1,162.558570565,6.32445379807,3.792,0.0281577188683,0.018,0.0737,0.0061,0.0273,90000,1.92787834743,79.9949521754,52.7439245113,7.66523093275,0 8 | adolescent#007,7,0,0,0,238.918844,87.9615903151,5.2342344,0,111.9,111.9,4.7237153349,94.4307701512,88.4032741841,238.918844,37.898,3.0324,145.16,111.9,0.053959,0.046226,0.0083388,0.63139,0.17323,1.6459,0.046776,5.2342344,0.03753,213.24,0.15372,0.065101,0.028587,0.17352,0.017336,0.694,0.6,0.391489481686,0.156595792674,0.26028,4.7237153349,0.0058301,0.0096939,0.0095992,0.9,238.918844,0.0005,339,1,87.9615903151,6.95942524417,3.0324,0.020890052356,0.0176,0.0503,0.0041,0.0188,90000,2.04914771228,94.4307701512,88.4032741841,6.42260586185,0 9 | adolescent#008,8,0,0,0,263.83812,33.9274151265,4.730160786,0,82.914,82.914,2.58030593927,64.7483947298,75.6122193488,263.83812,41.218,2.921,144.3,82.914,0.031127,0.051587,0.0085102,0.68583,0.13088,1.8284,0.057049,4.730160786,0.050245,216.9,0.2045,0.033578,0.024807,0.20978,0.0078171,0.67272,0.6,0.286087833178,0.114435133271,0.31467,2.58030593927,0.015816,0.0038941,0.0045499,0.9,263.83812,0.0005,339,1,33.9274151265,14.202068229,2.921,0.0202425502426,0.0174,0.0804,0.0035,0.0149,90000,1.35324144985,64.7483947298,75.6122193488,4.32566243169,0 10 | adolescent#009,9,0,0,0,241.4475,184.846172962,4.707832836,0,84.982,84.982,4.62123641991,71.2301660032,105.269360376,241.4475,43.885,4.2126,137.97,84.982,0.20973,0.027936,0.012496,0.87667,0.1589,1.75,0.055398,4.707832836,0.076427,223.83,0.080139,0.074658,0.035186,0.11961,0.028412,0.7136,0.6,0.29352469992,0.117409879968,0.179415,4.62123641991,0.0078732,0.014674,0.026073,0.9,241.4475,0.0005,339,1,184.846172962,7.10273332804,4.2126,0.0305327245053,0.0167,0.0912,0.0027,0.0113,90000,1.38186522046,71.2301660032,105.269360376,9.971336301,0 11 | adolescent#010,10,0,0,0,267.183741,169.7110728,6.32186544,0,108.24,108.24,3.58883085798,93.319683293,77.6764993884,267.183741,47.378,2.8311,144.87,108.24,0.80212,0.017759,0.010463,0.90795,0.26552,1.8443,0.058406,6.32186544,0.065509,231.53,0.13124,0.090215,0.038376,0.18514,0.024317,0.72708,0.6,0.262753198147,0.105101279259,0.27771,3.58883085798,0.010331,0.0028192,0.01386,0.9,267.183741,0.0005,339,1,169.7110728,4.32919618197,2.8311,0.0195423483123,0.0144,0.1045,0.0034,0.0173,90000,1.66109036262,93.319683293,77.6764993884,5.08455080263,0 12 | adult#001,1,0,0,0,265.370112,162.457097269,5.5043265,0,100.25,100.25,3.20762505142,72.4341762342,141.153779328,265.370112,102.32,2.2758,138.56,100.25,0.08906,0.046122,0.0037927,0.70391,0.21057,1.9152,0.054906,5.5043265,0.031319,253.52,0.087114,0.058138,0.027802,0.15446,0.027345,1.2642,0.6,0.225027424083,0.090010969633,0.23169,3.20762505142,0.0046374,0.00469,0.01208,0.9,265.370112,0.0005,339,1,162.457097269,3.2667306607,2.2758,0.0164246535797,0.0152,0.0766,0.0019,0.0078,90000,1.2386244136,72.4341762342,141.153779328,4.73140582528,0 13 | adult#002,2,0,0,0,228.74478,105.42424102,4.2367599,0,107.07,107.07,3.00055352824,65.2223493778,53.2999844378,228.74478,111.1,2.2639,136.45,107.07,0.021419,0.046859,0.0065141,0.62708,0.20388,1.6764,0.03957,4.2367599,0.041095,223.52,0.10712,0.054895,0.023888,0.16433,0.012082,1.2791,0.6,0.290954038543,0.116381615417,0.246495,3.00055352824,0.0029398,0.006551,0.010539,0.9,228.74478,0.0005,339,1,105.42424102,3.94361507564,2.2639,0.0165914254306,0.0152,0.1043,0.0037,0.0186,90000,1.23270240324,65.2223493778,53.2999844378,4.89081778378,0 14 | adult#003,3,0,0,0,273.07644,58.5083508878,4.7787333,0,131.45,131.45,4.76800378515,86.0119697596,89.3201224427,273.07644,81.631,2.7883,147.1,131.45,0.16971,0.042043,0.0056532,0.86326,0.20675,1.8564,0.036354,4.7787333,0.074874,244.83,0.22841,0.055487,0.037892,0.14648,0.01138,1.0843,0.6,0.365377784552,0.146151113821,0.21972,4.76800378515,0.016192,0.0025593,0.009496,0.9,273.07644,0.0005,339,1,58.5083508878,9.27149654129,2.7883,0.0189551325629,0.0162,0.0932,0.0041,0.0156,90000,1.74604298612,86.0119697596,89.3201224427,4.73543373289,0 15 | adult#004,42,0,0,0,284.241455193,206.107511651,6.64995399539,0,83.69716464,83.69716464,1.67041318173,57.5206305277,85.0483608517,284.241455193,63.0,2.986775436,150.6888823,83.69716464,0.080911441,0.058161342,0.00861298,0.637997241,0.092495502,1.886280201,0.07945256,6.64995399539,0.1086259,246.8818868,0.119405713,0.093572522,0.042193533,0.337462722,0.049867918,1.121391229,0.6,0.211919578527,0.0847678314109,0.506194083,1.67041318173,0.010115704,0.003087739,0.005633774,0.9,284.241455193,0.0005,339,1,206.107511651,4.36659587223,2.986775436,0.0198208082137,0.0207,0.0776,0.0038,0.014,90000,1.40925544793,57.5206305277,85.0483608517,4.33596977264,0 16 | adult#005,5,0,0,0,269.532164,85.3085339165,4.23964153,0,91.787,91.787,1.76058270751,67.0669032991,59.1299916661,269.532164,94.074,2.3574,142.67,91.787,0.039026,0.046023,0.0045175,0.7092,0.18486,1.8892,0.04619,4.23964153,0.024825,240.43,0.19683,0.067334,0.036957,0.28494,0.020251,1.2854,0.6,0.295815361468,0.118326144587,0.42741,1.76058270751,0.010603,0.0042297,0.0071329,0.9,269.532164,0.0005,339,1,85.3085339165,5.18303932372,2.3574,0.0165234457139,0.0149,0.0898,0.0038,0.0169,90000,1.25415109169,67.0669032991,59.1299916661,4.15214768637,0 17 | adult#006,6,0,0,0,261.839456,257.61167307,7.48715156,0,138.04,138.04,5.63793484811,169.421772684,174.84326941,261.839456,66.097,2.2226,135.64,138.04,0.58151,0.029515,0.014852,0.96206,0.1446,1.9304,0.054239,7.48715156,0.045288,188.77,0.070249,0.073784,0.053212,0.18511,0.029945,1.2493,0.6,0.348476356919,0.139390542768,0.277665,5.63793484811,0.011951,0.0026595,0.0046593,0.9,261.839456,0.0005,339,1,257.61167307,2.11848410047,2.2226,0.0163860218225,0.0129,0.0536,0.0025,0.0125,90000,2.60909529933,169.421772684,174.84326941,3.56213180523,0 18 | adult#007,7,0,0,0,249.82522,182.033173951,7.8531681,0,118.61,118.61,2.13475224365,83.519216599,94.8225692214,249.82522,91.229,2.8732,135.26,118.61,0.58034,0.032851,0.013232,0.93322,0.10058,1.847,0.06621,7.8531681,0.10286,212.78,0.11462,0.091015,0.04091,0.28169,0.050187,1.1563,0.6,0.19143177373,0.076572709492,0.422535,2.13475224365,0.01011,0.0024355,0.0069361,0.9,249.82522,0.0005,339,1,182.033173951,4.06279812296,2.8732,0.0212420523436,0.0151,0.0958,0.0029,0.0133,90000,1.50334589878,83.519216599,94.8225692214,4.30434014431,0 19 | adult#008,8,0,0,0,225.085945,142.685395142,6.7813815,0,106.1,106.1,2.68222815336,55.8011284167,51.2272654317,225.085945,102.79,2.8615,143.23,106.1,0.14058,0.024009,0.0048009,0.81105,0.17117,1.5715,0.063915,6.7813815,0.036819,264.01,0.065641,0.049881,0.035026,0.1656,0.034156,1.0758,0.6,0.163748707471,0.0654994829883,0.2484,2.68222815336,0.0049051,0.0080204,0.023879,0.9,225.085945,0.0005,339,1,142.685395142,5.30582318745,2.8615,0.0199783564896,0.0168,0.1519,0.0031,0.0183,90000,1.11044245549,55.8011284167,51.2272654317,7.20034121328,0 20 | adult#009,9,0,0,0,265.56894,73.3005106164,4.21939736,0,115.24,115.24,5.11019991428,73.7754104129,47.8945204697,265.56894,74.604,2.0874,145.08,115.24,0.21433,0.02187,0.0028379,0.76528,0.33742,1.8305,0.036614,4.21939736,0.016839,246.44,0.14598,0.044387,0.024128,0.11896,0.0070586,0.98387,0.6,0.360187326492,0.144074930597,0.17844,5.11019991428,0.0048098,0.0041804,0.0098864,0.9,265.56894,0.0005,339,1,73.3005106164,4.74329344121,2.0874,0.0143879239041,0.0161,0.1216,0.0045,0.0248,90000,1.51977345451,73.7754104129,47.8945204697,4.33689313278,0 21 | adult#010,10,0,0,0,317.320929,65.7778760652,4.413951432,0,92.373,92.373,3.44056216842,70.7300184239,67.3818518713,317.320929,73.859,2.3594,152.83,92.373,0.17966,0.039728,0.0091182,0.83824,0.13833,2.0763,0.047784,4.413951432,0.054346,246.13,0.1569,0.036808,0.016772,0.16035,0.023357,1.1028,0.6,0.31247180231,0.124988720924,0.240525,3.44056216842,0.008067,0.0034404,0.0048981,0.9,317.320929,0.0005,339,1,65.7778760652,6.44605134867,2.3594,0.0154380684421,0.0161,0.0893,0.0034,0.0169,90000,1.37923535927,70.7300184239,67.3818518713,3.90356311543,0 22 | child#001,100,0,0,0,271.031853002,137.60135923,7.39593844866,0,110.3648078,110.3648078,1.45378246253,66.4071837276,35.7997574828,271.031853002,34.55648182,2.556787173,141.2046604,110.3648078,0.127494918,0.068714386,0.007997018,0.754915658,0.089529251,1.919425692,0.067013558,7.39593844866,0.152308243,185.8820726,0.128243663,0.070852519,0.042802541,0.314270832,0.047703625,0.35763698,0.6,0.154436596254,0.0617746385016,0.471406248,1.45378246253,0.010902381,0.002522654,0.008379266,0.9,271.031853002,0.0005,339,1,137.60135923,3.65981019497,2.556787173,0.0181069602502,0.0131,0.0684,0.0041,0.0243,90000,1.14220356012,66.4071837276,35.7997574828,4.1652828427,0 23 | child#002,16,0,0,0,264.366111171,384.348662938,3.90569033089,0,104.3351578,104.3351578,2.72414938639,68.8906316382,96.4468842935,264.366111171,28.53257352,3.579170179,143.3925294,104.3351578,0.547271474,0.020390384,0.011239842,0.83267505,0.168209241,1.843653308,0.037434077,3.90569033089,0.062822379,225.7855513,0.067069068,0.107264417,0.067564011,0.203322432,0.020325939,0.378674876,0.6,0.354534430182,0.141813772073,0.304983648,2.72414938639,0.006425442,0.008198363,0.023930547,0.9,264.366111171,0.0005,339,1,384.348662938,4.09430322593,3.579170179,0.0249606460949,0.0168,0.0617,0.0033,0.012,90000,1.38470169593,68.8906316382,96.4468842935,8.24333692076,0 24 | child#003,41,0,0,0,233.426051066,90.1365395679,5.59519238156,0,96.38653336,96.38653336,1.79509115018,26.8346975874,17.5645293299,233.426051066,41.23304017,2.167653937,133.8310731,96.38653336,0.099733323,0.053017395,0.006550925,0.595219309,0.119133938,1.744184259,0.058049524,5.59519238156,0.063997085,260.6177434,0.151559162,0.06352621,0.06520304,0.156066862,0.020572923,0.299616833,0.6,0.125176322684,0.0500705290734,0.234100293,1.79509115018,0.002815868,0.003133049,0.011524925,0.9,233.426051066,0.0005,339,1,90.1365395679,4.54376905737,2.167653937,0.0161969405669,0.018,0.0678,0.0081,0.0275,90000,0.70038560703,26.8346975874,17.5645293299,4.00983676085,0 25 | child#004,61,0,0,0,276.767871791,340.969887233,7.52405238182,0,124.0496989,124.0496989,2.42440010999,67.2868436092,57.1763852431,276.767871791,35.5165043,2.366618286,133.7160874,124.0496989,0.233436885,0.030653413,0.016230469,0.778097629,0.095402818,2.069817306,0.060653532,7.52405238182,0.145715992,203.0817342,0.055357799,0.07313696,0.081707271,0.228693106,0.024671457,0.396855018,0.6,0.184223727854,0.0736894911417,0.343039659,2.42440010999,0.0074842,0.002691865,0.010417071,0.9,276.767871791,0.0005,339,1,340.969887233,2.18057641516,2.366618286,0.0176988299016,0.0164,0.1077,0.0042,0.0193,90000,1.38610897835,67.2868436092,57.1763852431,4.40387455417,0 26 | child#005,62,0,0,0,256.487996958,92.9517456616,4.34091457232,0,119.291148,119.291148,1.89567530255,58.2559242046,57.6704375292,256.487996958,37.78855797,2.581818256,137.8702919,119.291148,0.330887558,0.013142286,0.012509418,0.960638488,0.201231635,1.860357249,0.036389243,4.34091457232,0.109076135,230.2404808,0.137397884,0.05596048,0.042999249,0.287641797,0.013546858,0.431825272,0.6,0.314032585456,0.125613034182,0.4314626955,1.89567530255,0.00697472,0.001989104,0.007058817,0.9,256.487996958,0.0005,339,1,92.9517456616,5.49996517414,2.581818256,0.0187264291706,0.0197,0.1244,0.0037,0.0199,90000,1.36318862639,58.2559242046,57.6704375292,3.93405394015,0 27 | child#006,64,0,0,0,242.193130522,102.965023189,5.99228401573,0,100.4220798,100.4220798,1.70344929141,47.1524941769,39.6456294323,242.193130522,41.00214896,2.742872391,142.0005769,100.4220798,0.052654086,0.04693107,0.006665559,0.632999169,0.148305924,1.705578497,0.059670981,5.99228401573,0.081494569,238.0059149,0.116375837,0.056671769,0.067333073,0.231409795,0.050894003,0.402372567,0.6,0.164459349008,0.0657837396032,0.3471146925,1.70344929141,0.009870995,0.002702742,0.013575452,0.9,242.193130522,0.0005,339,1,102.965023189,5.77156024175,2.742872391,0.0193159242792,0.0169,0.0943,0.004,0.0201,90000,0.985487128297,47.1524941769,39.6456294323,4.76073306104,0 28 | child#007,68,0,0,0,231.016602659,114.376709557,7.73719352451,0,104.4935749,104.4935749,1.42326269189,51.5538430194,59.1246870991,231.016602659,45.5397665,3.889183779,133.4001679,104.4935749,0.032863284,0.066365919,0.008004161,0.951778007,0.142843935,1.731756461,0.074044682,7.73719352451,0.073813859,225.0909871,0.104299715,0.064145355,0.043035605,0.288329479,0.049902688,0.447110978,0.6,0.132596072831,0.0530384291325,0.4324942185,1.42326269189,0.011469231,0.004102445,0.006996318,0.9,231.016602659,0.0005,339,1,114.376709557,8.57503740466,3.889183779,0.0291542644977,0.0164,0.0792,0.0035,0.0143,90000,1.02592147609,51.5538430194,59.1246870991,5.56798696445,0 29 | child#008,88,0,0,0,307.226606718,74.4414259764,5.31506447574,0,100.6946287,100.6946287,2.63125641834,82.3409671628,119.914029849,307.226606718,23.73405728,2.89615241,151.3088739,100.6946287,0.125140051,0.042336118,0.007525726,0.62906403,0.212803068,2.030459938,0.052783992,5.31506447574,0.080044566,224.1030036,0.141265983,0.040400777,0.034704085,0.217802084,0.012471352,0.33769987,0.6,0.269560761712,0.107824304685,0.326703126,2.63125641834,0.008655602,0.004799275,0.010880209,0.9,307.226606718,0.0005,339,1,74.4414259764,7.60444513533,2.89615241,0.0191406646243,0.015,0.1064,0.0024,0.0103,90000,1.43273282863,82.3409671628,119.914029849,5.46619598839,0 30 | child#009,89,0,0,0,260.782139377,378.358611322,5.4319759636,0,107.0898879,107.0898879,3.36507807403,54.5323874938,31.539253961,260.782139377,35.53392558,3.391235468,137.272603,107.0898879,0.163226227,0.067388647,0.013139851,0.808284982,0.134892405,1.899739159,0.050723519,5.4319759636,0.113583485,268.3161828,0.049496663,0.080982249,0.041367326,0.130939515,0.032686792,0.36551113,0.6,0.202790703559,0.0811162814237,0.1964092725,3.36507807403,0.012533054,0.015376442,0.01421148,0.9,260.782139377,0.0005,339,1,378.358611322,4.08700015724,3.391235468,0.0247043867013,0.0155,0.0888,0.0047,0.0268,90000,1.10155422738,54.5323874938,31.539253961,8.92304270886,0 31 | child#010,100,0,0,0,247.420085943,118.336141455,6.56877346313,0,105.2945301,105.2945301,2.3252309291,60.0485027983,54.4625955612,247.420085943,35.21305847,3.376363824,136.4151774,105.2945301,0.217656178,0.036428394,0.019217714,0.678361137,0.097283767,1.813728433,0.062384755,6.56877346313,0.09197894,226.4829255,0.102776369,0.058760479,0.036427048,0.194202105,0.03026706,0.377535652,0.6,0.171860372251,0.0687441489005,0.2913031575,2.3252309291,0.007965987,0.003572135,0.005341234,0.9,247.420085943,0.0005,339,1,118.336141455,6.92447418397,3.376363824,0.0247506464336,0.0156,0.0947,0.0032,0.0172,90000,1.12891185261,60.0485027983,54.4625955612,4.82258449688,0 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # simglucose 2 | 3 | [![Downloads](https://static.pepy.tech/badge/simglucose)](https://pepy.tech/project/simglucose) 4 | [![Downloads](https://static.pepy.tech/badge/simglucose/month)](https://pepy.tech/project/simglucose) 5 | [![Downloads](https://static.pepy.tech/badge/simglucose/week)](https://pepy.tech/project/simglucose) 6 | 7 | A Type-1 Diabetes simulator implemented in Python for Reinforcement Learning purpose 8 | 9 | This simulator is a python implementation of the FDA-approved [UVa/Padova Simulator (2008 version)](https://www.ncbi.nlm.nih.gov/pmc/articles/PMC4454102/) for research purpose only. The simulator includes 30 virtual patients, 10 adolescents, 10 adults, 10 children. There is [documentation of the virtual patient's parameters](https://github.com/jxx123/simglucose/blob/master/definitions_of_vpatient_parameters.md). 10 | 11 | **HOW TO CITE**: Jinyu Xie. Simglucose v0.2.1 (2018) \[Online\]. Available: https://github.com/jxx123/simglucose. Accessed on: Month-Date-Year. 12 | 13 | **Notice**: simglucose no longer supports python 3.7 and 3.8, please update to >=3.9 verison. Thanks! 14 | 15 | **Announcement (08/20/2023)**: simglucose now supports gymnasium! Check [examples/run_gymnasium.py](examples/run_gymnasium.py) for usage. 16 | 17 | | Animation | CVGA Plot | BG Trace Plot | Risk Index Stats | 18 | | ------------------------------------------------ | :---------------------------- | ----------------------------------------------- | ----------------------------------------------- | 19 | | ![animation screenshot](screenshots/animate.png) | ![CVGA](screenshots/CVGA.png) | ![BG Trace Plot](screenshots/BG_trace_plot.png) | ![Risk Index Stats](screenshots/risk_index.png) | 20 | 21 | 22 | 23 | ## Main Features 24 | 25 | - Simulation environment follows [OpenAI gym](https://github.com/openai/gym) and [rllab](https://github.com/rll/rllab) APIs. It returns observation, reward, done, info at each step, which means the simulator is "reinforcement-learning-ready". 26 | - Supports customized reward function. The reward function is a function of blood glucose measurements in the last hour. By default, the reward at each step is `risk[t-1] - risk[t]`. `risk[t]` is the risk index at time `t` defined in this [paper](https://www.ncbi.nlm.nih.gov/pmc/articles/PMC2903980/pdf/dia.2008.0138.pdf). 27 | - Supports parallel computing. The simulator simulates multiple patients in parallel using [pathos multiprocessing package](https://github.com/uqfoundation/pathos) (you are free to turn parallel off by setting `parallel=False`). 28 | - The simulator provides a random scenario generator (`from simglucose.simulation.scenario_gen import RandomScenario`) and a customized scenario generator (`from simglucose.simulation.scenario import CustomScenario`). Commandline user-interface will guide you through the scenario settings. 29 | - The simulator provides the most basic basal-bolus controller for now. It provides very simple syntax to implement your own controller, like Model Predictive Control, PID control, reinforcement learning control, etc. 30 | - You can specify random seed in case you want to repeat your experiments. 31 | - The simulator will generate several plots for performance analysis after simulation. The plots include blood glucose trace plot, Control Variability Grid Analysis (CVGA) plot, statistics plot of blood glucose in different zones, risk indices statistics plot. 32 | - NOTE: `animate` and `parallel` cannot be set to `True` at the same time in macOS. Most backends of matplotlib in macOS is not thread-safe. Windows has not been tested. Let me know the results if anybody has tested it out. 33 | 34 | ## Installation 35 | 36 | It is highly recommended using `pip` to install `simglucose`, follow this [link](https://pip.pypa.io/en/stable/installing/) to install pip. 37 | 38 | Auto installation: 39 | 40 | ```bash 41 | pip install simglucose 42 | ``` 43 | 44 | Manual installation: 45 | 46 | ```bash 47 | git clone https://github.com/jxx123/simglucose.git 48 | cd simglucose 49 | ``` 50 | 51 | If you have `pip` installed, then 52 | 53 | ```bash 54 | pip install -e . 55 | ``` 56 | 57 | If you do not have `pip`, then 58 | 59 | ```bash 60 | python setup.py install 61 | ``` 62 | 63 | If [rllab (optional)](https://github.com/rll/rllab) is installed, the package will utilize some functionalities in rllab. 64 | 65 | Note: there might be some minor differences between auto install version and manual install version. Use `git clone` and manual installation to get the latest version. 66 | 67 | ## Quick Start 68 | 69 | ### Use simglucose as a simulator and test controllers 70 | 71 | Run the simulator user interface 72 | 73 | ```python 74 | from simglucose.simulation.user_interface import simulate 75 | simulate() 76 | ``` 77 | 78 | You are free to implement your own controller, and test it in the simulator. For example, 79 | 80 | ```python 81 | from simglucose.simulation.user_interface import simulate 82 | from simglucose.controller.base import Controller, Action 83 | 84 | 85 | class MyController(Controller): 86 | def __init__(self, init_state): 87 | self.init_state = init_state 88 | self.state = init_state 89 | 90 | def policy(self, observation, reward, done, **info): 91 | ''' 92 | Every controller must have this implementation! 93 | ---- 94 | Inputs: 95 | observation - a namedtuple defined in simglucose.simulation.env. For 96 | now, it only has one entry: blood glucose level measured 97 | by CGM sensor. 98 | reward - current reward returned by environment 99 | done - True, game over. False, game continues 100 | info - additional information as key word arguments, 101 | simglucose.simulation.env.T1DSimEnv returns patient_name 102 | and sample_time 103 | ---- 104 | Output: 105 | action - a namedtuple defined at the beginning of this file. The 106 | controller action contains two entries: basal, bolus 107 | ''' 108 | self.state = observation 109 | action = Action(basal=0, bolus=0) 110 | return action 111 | 112 | def reset(self): 113 | ''' 114 | Reset the controller state to inital state, must be implemented 115 | ''' 116 | self.state = self.init_state 117 | 118 | 119 | ctrller = MyController(0) 120 | simulate(controller=ctrller) 121 | ``` 122 | 123 | These two examples can also be found in examples\ folder. 124 | 125 | In fact, you can specify a lot more simulation parameters through `simulation`: 126 | 127 | ```python 128 | simulate(sim_time=my_sim_time, 129 | scenario=my_scenario, 130 | controller=my_controller, 131 | start_time=my_start_time, 132 | save_path=my_save_path, 133 | animate=False, 134 | parallel=True) 135 | ``` 136 | 137 | ### OpenAI Gym usage 138 | 139 | - Using default reward 140 | 141 | ```python 142 | import gym 143 | 144 | # Register gym environment. By specifying kwargs, 145 | # you are able to choose which patient or patients to simulate. 146 | # patient_name must be 'adolescent#001' to 'adolescent#010', 147 | # or 'adult#001' to 'adult#010', or 'child#001' to 'child#010' 148 | # It can also be a list of patient names 149 | # You can also specify a custom scenario or a list of custom scenarios 150 | # If you chose a list of patient names or a list of custom scenarios, 151 | # every time the environment is reset, a random patient and scenario will be 152 | # chosen from the list 153 | 154 | from gym.envs.registration import register 155 | from simglucose.simulation.scenario import CustomScenario 156 | from datetime import datetime 157 | 158 | start_time = datetime(2018, 1, 1, 0, 0, 0) 159 | meal_scenario = CustomScenario(start_time=start_time, scenario=[(1,20)]) 160 | 161 | 162 | register( 163 | id='simglucose-adolescent2-v0', 164 | entry_point='simglucose.envs:T1DSimEnv', 165 | kwargs={'patient_name': 'adolescent#002', 166 | 'custom_scenario': meal_scenario} 167 | ) 168 | 169 | env = gym.make('simglucose-adolescent2-v0') 170 | 171 | observation = env.reset() 172 | for t in range(100): 173 | env.render(mode='human') 174 | print(observation) 175 | # Action in the gym environment is a scalar 176 | # representing the basal insulin, which differs from 177 | # the regular controller action outside the gym 178 | # environment (a tuple (basal, bolus)). 179 | # In the perfect situation, the agent should be able 180 | # to control the glucose only through basal instead 181 | # of asking patient to take bolus 182 | action = env.action_space.sample() 183 | observation, reward, done, info = env.step(action) 184 | if done: 185 | print("Episode finished after {} timesteps".format(t + 1)) 186 | break 187 | ``` 188 | 189 | - Customized reward function 190 | 191 | ```python 192 | import gym 193 | from gym.envs.registration import register 194 | 195 | 196 | def custom_reward(BG_last_hour): 197 | if BG_last_hour[-1] > 180: 198 | return -1 199 | elif BG_last_hour[-1] < 70: 200 | return -2 201 | else: 202 | return 1 203 | 204 | 205 | register( 206 | id='simglucose-adolescent2-v0', 207 | entry_point='simglucose.envs:T1DSimEnv', 208 | kwargs={'patient_name': 'adolescent#002', 209 | 'reward_fun': custom_reward} 210 | ) 211 | 212 | env = gym.make('simglucose-adolescent2-v0') 213 | 214 | reward = 1 215 | done = False 216 | 217 | observation = env.reset() 218 | for t in range(200): 219 | env.render(mode='human') 220 | action = env.action_space.sample() 221 | observation, reward, done, info = env.step(action) 222 | print(observation) 223 | print("Reward = {}".format(reward)) 224 | if done: 225 | print("Episode finished after {} timesteps".format(t + 1)) 226 | break 227 | ``` 228 | 229 | ### rllab usage 230 | 231 | ```python 232 | from rllab.algos.ddpg import DDPG 233 | from rllab.envs.normalized_env import normalize 234 | from rllab.exploration_strategies.ou_strategy import OUStrategy 235 | from rllab.policies.deterministic_mlp_policy import DeterministicMLPPolicy 236 | from rllab.q_functions.continuous_mlp_q_function import ContinuousMLPQFunction 237 | from rllab.envs.gym_env import GymEnv 238 | from gym.envs.registration import register 239 | 240 | register( 241 | id='simglucose-adolescent2-v0', 242 | entry_point='simglucose.envs:T1DSimEnv', 243 | kwargs={'patient_name': 'adolescent#002'} 244 | ) 245 | 246 | env = GymEnv('simglucose-adolescent2-v0') 247 | env = normalize(env) 248 | 249 | policy = DeterministicMLPPolicy( 250 | env_spec=env.spec, 251 | # The neural network policy should have two hidden layers, each with 32 hidden units. 252 | hidden_sizes=(32, 32) 253 | ) 254 | 255 | es = OUStrategy(env_spec=env.spec) 256 | 257 | qf = ContinuousMLPQFunction(env_spec=env.spec) 258 | 259 | algo = DDPG( 260 | env=env, 261 | policy=policy, 262 | es=es, 263 | qf=qf, 264 | batch_size=32, 265 | max_path_length=100, 266 | epoch_length=1000, 267 | min_pool_size=10000, 268 | n_epochs=1000, 269 | discount=0.99, 270 | scale_reward=0.01, 271 | qf_learning_rate=1e-3, 272 | policy_learning_rate=1e-4 273 | ) 274 | algo.train() 275 | ``` 276 | 277 | ## Advanced Usage 278 | 279 | You can create the simulation objects, and run batch simulation. For example, 280 | 281 | ```python 282 | from simglucose.simulation.env import T1DSimEnv 283 | from simglucose.controller.basal_bolus_ctrller import BBController 284 | from simglucose.sensor.cgm import CGMSensor 285 | from simglucose.actuator.pump import InsulinPump 286 | from simglucose.patient.t1dpatient import T1DPatient 287 | from simglucose.simulation.scenario_gen import RandomScenario 288 | from simglucose.simulation.scenario import CustomScenario 289 | from simglucose.simulation.sim_engine import SimObj, sim, batch_sim 290 | from datetime import timedelta 291 | from datetime import datetime 292 | 293 | # specify start_time as the beginning of today 294 | now = datetime.now() 295 | start_time = datetime.combine(now.date(), datetime.min.time()) 296 | 297 | # --------- Create Random Scenario -------------- 298 | # Specify results saving path 299 | path = './results' 300 | 301 | # Create a simulation environment 302 | patient = T1DPatient.withName('adolescent#001') 303 | sensor = CGMSensor.withName('Dexcom', seed=1) 304 | pump = InsulinPump.withName('Insulet') 305 | scenario = RandomScenario(start_time=start_time, seed=1) 306 | env = T1DSimEnv(patient, sensor, pump, scenario) 307 | 308 | # Create a controller 309 | controller = BBController() 310 | 311 | # Put them together to create a simulation object 312 | s1 = SimObj(env, controller, timedelta(days=1), animate=False, path=path) 313 | results1 = sim(s1) 314 | print(results1) 315 | 316 | # --------- Create Custom Scenario -------------- 317 | # Create a simulation environment 318 | patient = T1DPatient.withName('adolescent#001') 319 | sensor = CGMSensor.withName('Dexcom', seed=1) 320 | pump = InsulinPump.withName('Insulet') 321 | # custom scenario is a list of tuples (time, meal_size) 322 | scen = [(7, 45), (12, 70), (16, 15), (18, 80), (23, 10)] 323 | scenario = CustomScenario(start_time=start_time, scenario=scen) 324 | env = T1DSimEnv(patient, sensor, pump, scenario) 325 | 326 | # Create a controller 327 | controller = BBController() 328 | 329 | # Put them together to create a simulation object 330 | s2 = SimObj(env, controller, timedelta(days=1), animate=False, path=path) 331 | results2 = sim(s2) 332 | print(results2) 333 | 334 | 335 | # --------- batch simulation -------------- 336 | # Re-initialize simulation objects 337 | s1.reset() 338 | s2.reset() 339 | 340 | # create a list of SimObj, and call batch_sim 341 | s = [s1, s2] 342 | results = batch_sim(s, parallel=True) 343 | print(results) 344 | ``` 345 | 346 | Run analysis offline (example/offline_analysis.py): 347 | 348 | ```python 349 | from simglucose.analysis.report import report 350 | import pandas as pd 351 | from pathlib import Path 352 | 353 | 354 | # get the path to the example folder 355 | exmaple_pth = Path(__file__).parent 356 | 357 | # find all csv with pattern *#*.csv, e.g. adolescent#001.csv 358 | result_filenames = list(exmaple_pth.glob( 359 | 'results/2017-12-31_17-46-32/*#*.csv')) 360 | patient_names = [f.stem for f in result_filenames] 361 | df = pd.concat( 362 | [pd.read_csv(str(f), index_col=0) for f in result_filenames], 363 | keys=patient_names) 364 | report(df) 365 | ``` 366 | 367 | ## Release Notes 368 | 369 | ### 08/20/2023 370 | 371 | - Fixed numpy compatibility issues for risk index computation (thanks to @yihuicai) 372 | - Support Gymnasium. 373 | - **NOTE**: the observation in gymnasium version is no longer a namedtuple with a CGM field. It is a numpy array instead (to be consistent with its space definition). 374 | - **NOTE**: Python 3.7 and 3.8 are no longer supported. Please update to >3.9 version. 375 | 376 | ### 03/10/2021 377 | 378 | - Fixed some random seed issues. 379 | 380 | ### 5/27/2020 381 | 382 | - Add PIDController at simglucose/controller/pid_ctrller. There is an example at examples/run_pid_controller.py showing how to use it. 383 | 384 | ### 9/10/2018 385 | 386 | - Controller `policy` method gets access to all the current patient state through `info['patient_state']`. 387 | 388 | ### 2/26/2018 389 | 390 | - Support customized reward function. 391 | 392 | ### 1/10/2018 393 | 394 | - Added workaround to select patient when make gym environment: register gym environment by passing kwargs of patient_name. 395 | 396 | ### 1/7/2018 397 | 398 | - Added OpenAI gym support, use `gym.make('simglucose-v0')` to make the environment. 399 | - Noticed issue: the patient name selection is not available in gym.make for now. The patient name has to be hard-coded in the constructor of `simglucose.envs.T1DSimEnv`. 400 | 401 | ## Reporting issues 402 | 403 | Shoot me any bugs, enhancements or even discussion by [creating issues](https://github.com/jxx123/simglucose/issues/new). 404 | 405 | ## How to contribute 406 | 407 | The following instruction is originally from the [contribution instructions of sklearn](https://github.com/scikit-learn/scikit-learn/blob/master/CONTRIBUTING.md). 408 | 409 | The preferred workflow for contributing to simglucose is to fork the 410 | [main repository](https://github.com/jxx123/simglucose) on 411 | GitHub, clone, and develop on a branch. Steps: 412 | 413 | 1. Fork the [project repository](https://github.com/jxx123/simglucose) 414 | by clicking on the 'Fork' button near the top right of the page. This creates 415 | a copy of the code under your GitHub user account. For more details on 416 | how to fork a repository see [this guide](https://help.github.com/articles/fork-a-repo/). 417 | 418 | 2. Clone your fork of the simglucose repo from your GitHub account to your local disk: 419 | 420 | ```bash 421 | $ git clone git@github.com:YourLogin/simglucose.git 422 | $ cd simglucose 423 | ``` 424 | 425 | 3. Create a `feature` branch to hold your development changes: 426 | 427 | ```bash 428 | $ git checkout -b my-feature 429 | ``` 430 | 431 | Always use a `feature` branch. It's good practice to **never work on the `master` branch**! 432 | 433 | 4. Develop the feature on your feature branch. Add changed files using `git add` and then `git commit` files: 434 | 435 | ```bash 436 | $ git add modified_files 437 | $ git commit 438 | ``` 439 | 440 | to record your changes in Git, then push the changes to your GitHub account with: 441 | 442 | ```bash 443 | $ git push -u origin my-feature 444 | ``` 445 | 446 | 5. Follow [these instructions](https://help.github.com/articles/creating-a-pull-request-from-a-fork) 447 | to create a pull request from your fork. This will email the committers. 448 | 449 | (If any of the above seems like magic to you, please look up the 450 | [Git documentation](https://git-scm.com/documentation) on the web, or ask a friend or another contributor for help.) 451 | --------------------------------------------------------------------------------