Skip to content

Commit 8212b34

Browse files
authored
Merge pull request #10 from openAFT/dev
dev
2 parents cff9c99 + a6e283e commit 8212b34

30 files changed

Lines changed: 1601 additions & 386 deletions

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ cover/
6363

6464
# OS
6565
.DS_Store
66+
*.pdf
6667

6768
# Django stuff:
6869
*.log

MANUAL.md

Lines changed: 70 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22

33
## Working tree
44

5-
One should perform calculations in a working folder e.g. `adaptfx/work`. There the instruction files can be specified by the user and `adaptfx` will automatically produce log files if the user wishes to.
5+
The package ships a CLI with which calculations can be performed which will be explained here. One should perform calculations in a working folder e.g. `adaptfx/work`. There the instruction files can be specified by the user and `adaptfx` will automatically produce log files if the user wishes to.
6+
7+
The package also also provides a class that can be used in scripting mode. When using the package in scripts, a class creates an object with according attributes. To understand the attributes and behaviour of optimisation, read the doc-string of the `aft.py` script [here](src/adaptfx/aft.py)
68

79
## Format of the instruction file
810
The user specifies following elements of the dictionary for the main entries:
@@ -94,7 +96,7 @@ min_dose : float
9496
max_dose : float
9597
maximal physical doses to be delivered in one fraction.
9698
The doses are aimed at PTV 95. If -1 the dose is adapted to the
97-
remaining dose tumor dose to be delivered or the remaining OAR dose
99+
remaining tumor BED to be delivered or the remaining OAR dose
98100
allowed to be prescribed.
99101
default: -1
100102
```
@@ -136,24 +138,86 @@ sf_high : float
136138
sf_stepsize: float
137139
stepsize of the sparing factor stepsize.
138140
sf_prob_threshold': float
139-
probability threshold of the sparing factor occuring.
141+
probability threshold of the sparing factor occuring
142+
which defines the range of sparing factors.
140143
inf_penalty : float
141-
infinite penalty for certain undesired states.
144+
define infinite penalty for undesired states
145+
choose arbitrarily large compared to highest occuring reward.
142146
plot_policy : int
143147
starting from which fraction policy should be plotted.
144148
plot_values : int
145149
starting from which fraction value should be plotted.
146150
plot_remains : int
147151
starting from which fraction expected remaining number
148152
of fractions should be plotted.
153+
plot_probability : int
154+
flag if the probability distribution should be plotted
149155
```
150156

151-
# Example
157+
> :warning: Note:\
158+
> It is dangerous to use an upper and lower bound in `sf_low` and `sf_high`, as a truncated probability distribution may result that not accurately represents the environment model. Best is to set `sf_prob_threshold` to `1e-3` or lower or leave it at the default which is set at `1e-4`.
159+
160+
## Note on Plots
161+
Policy, Value and Remaining number of fractions plots are calculated with the probability distribution in the fraction from which the plots should start. That is the value function that is known when iterating backwards through the fractions. E.g. the plotted policy starting to plot in the first fraction i.e `plot_policy = 1` and `prob_update = 0` is the policy which is known throughout the treatment, when observing only the first sparing factor. In the case of probability updating e.g `prob_update = 1` the plotted policy is the optimal policy for the probability distribution known when observing the first sparing factor. As the probability distribution changes also future optimal policies change and one has to keep in mind only policy with the constant probability distribution from fraction `1` is plotted.
162+
163+
Different is the probability plot: the probability is set for each fraction and updated with additional oberved sparing factors.
164+
165+
# AFT CLI
152166

153-
Outlined is an example instruction file for fraction minimisation. It simply is a `.json` that is translated into a python dictionary. An example can be found [here](work/oar_example.json)
167+
Outlined is an example instruction file for fraction minimisation. It simply is a `.json` that is translated into a python dictionary. An example can be found [here](work/example_0.json)
154168

155169
This `.json` file can be called in with the CLI as:
156170

157171
```
158172
$ aft -f work/oar_example.json
173+
```
174+
175+
# AST CLI
176+
There is also a second CLI that allows to plot sparing factors, policy functions, temporal Adaptive Fractionation Therapy etc.
177+
```
178+
$ ast [options] -f <simulation_file>
179+
```
180+
181+
The entry for algorithm simulation type is
182+
183+
```
184+
algorithm_simulation: histogram, fraction, single_state, all_state
185+
single_distance, single_patient, grid_distance, grid_fraction
186+
allowed plots options
187+
keys_simulation: dict
188+
simulation keys
189+
```
190+
191+
```
192+
keys_simulation
193+
----------------
194+
# Histogram of applied AFT for sampled patients
195+
n_patients : float
196+
stepsize of the actionspace.
197+
fixed_mean_sample : float
198+
mean of sampled patient sparing factor
199+
fixed_std_sample : float
200+
standard deviation of sampled patient sparing factor
201+
202+
# Data related plots
203+
c_list : list
204+
list of c parameters
205+
plot_index : int
206+
which index to plot
207+
data_filepath : string
208+
path to sparing factor file
209+
data_selection : list
210+
two elements of header in sparing factor file
211+
data_row_hue : string
212+
seaborn hue or row
213+
214+
# Settings of plots
215+
figsize : list
216+
two elements of size figure
217+
fontsize : float
218+
fontsize of plots
219+
save : bool
220+
boolean to instruct saving plot
221+
usetex : bool
222+
boolean if TeX font should be used
159223
```

README.md

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,10 @@ A last addition is made with graphical user interfaces that facilitate the use o
124124
> :warning: Note:\
125125
> The interfaces are not optimized, and thus it is not recommended using them to further develop extensions.
126126
127+
### Reducing Number of Fractions
128+
129+
For the 2D algorithms there exist the possibility to reduce number of fractions. A constant $c$ can be chosen which introduces a reward (or rather a cost) linear to the number of fractions used to finish the treatment. The cost is added to the immediate reward returned by the environment in the current fraction. There exist a simulative model helping to estimate what the constant $c$ should be chosen in order for the treatment to finish on some target number of fractions $n_{\text{targ}}$. The function can be found [here](src/adaptfx/radiobiology.py) in `c_calc`.
130+
127131
### Probability Updating
128132

129133
The DP algorithm relies on a description of the environment to compute an optimal policy, in this case the probability distribution of the sparing factor $P(\delta)$, which we assume to be a Gaussian distribution truncated at $0$, with patient-specific parameters for mean and standard deviation. At the start of a treatment, only two sparing factors are available for that patient, from the planning scan and the first fraction. In each fraction, an additional sparing factor is measured, which can be used to calculate updated estimates $\mu_t$ and $\sigma_t$ for mean and standard deviation, respectively.
@@ -173,12 +177,12 @@ ImportError: No module named '_ctypes'
173177
**Solution:** with the specific package manager of the Linux distribution install `libffi-dev` development tool. E.g. in Fedora Linux and derivatives install this tool
174178

175179
```
176-
sudo dnf install libffi-devel
180+
$ sudo dnf install libffi-devel
177181
```
178182

179183
On Ubuntu:
180184
```
181-
sudo apt install libffi-dev
185+
$ sudo apt install libffi-dev
182186
```
183187

184188
### No GUI backend for `matplotlib`
@@ -195,19 +199,19 @@ No matching distribution found for tkinter
195199
**Solution:** on Fedora Linux and derivative distributions one could solve this by either installing python tkinter
196200

197201
```
198-
sudo dnf install python3-tkinter
202+
$ sudo dnf install python3-tkinter
199203
```
200204

201205
on Ubuntu
202206

203207
```
204-
sudo apt-get install python3-tk
208+
$ sudo apt-get install python3-tk
205209
```
206210

207211
**Solution:** on MacOS and Linux one could instead use `pip` to install `pyqt`
208212

209213
```
210-
pip install pyqt5
214+
$ pip install pyqt5
211215
```
212216

213217

pyproject.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,9 @@ dependencies = [
2626
"numpy == 1.24.1",
2727
"scipy == 1.10.0",
2828
"matplotlib == 3.6.3",
29-
"pyqt6 == 6.4.0"
29+
"pyqt6 == 6.4.0",
30+
"pandas==1.5.2",
31+
"seaborn==0.12.2"
3032
]
3133
dynamic = ["version"]
3234

@@ -47,3 +49,4 @@ include = ["*"]
4749

4850
[project.scripts]
4951
aft = "adaptfx.aft:main"
52+
ast = "adaptsim.ast:main"

src/adaptfx/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from .radiobiology import *
1515
from .visualiser import *
1616
from .reinforce import *
17-
from .reinforce_old import *
17+
from .reinforce_old import max_tumor_bed_old, min_oar_bed_old, min_oar_max_tumor_old
1818

1919
__all__ = ['bed_calc_matrix',
2020
'convert_to_physical',

src/adaptfx/aft.py

Lines changed: 99 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,80 @@
77

88
class RL_object():
99
"""
10-
Reinforcement Learning class to check instructions
11-
of calculation, invoke keys and define
12-
calculation settings from file
13-
"""
14-
def __init__(self, instruction_filename):
10+
Invokes a class with one dictionary object and multiple functions
11+
to operate on the dictionary. Dictionary is created from instruction
12+
file and contains instructions, keys, settings. Once optimisation
13+
is performed it also provides results.
14+
15+
Parameters
16+
----------
17+
instruction_filename_in : string
18+
string of instruction filename
19+
20+
Returns
21+
-------
22+
returns : ``plan`` class
23+
The optimisation result represented as a ``plan`` object.
24+
Important attributes are: ``keys`` and ``settings`` the keys and
25+
settings from the instruction file. To start the optimisation one
26+
can run ``plan.optimise`` which will store the results in ``output``.
27+
Important attributes are: ``output.physical_doses``,
28+
``output.tumor_doses``, ``output.oar_doses``.
29+
If one is looking for summed doses they are found in:
30+
``output.tumor_sum``, ``output.oar_sum``.
31+
In case where number of fractions are minimised one can check
32+
utilised number of fractions with ``output.fractions_used``.
33+
34+
A full list of attributes:
35+
``plan.filename``
36+
``plan.basename``
37+
``plan.algorithm``
38+
``plan.log``
39+
``plan.log_level``
40+
``plan.keys``
41+
``plan.settings``
42+
``plan.output.physical_doses``
43+
``plan.output.tumor_doses``
44+
``plan.output.oar_doses``
45+
``plan.output.tumor_sum``
46+
``plan.output.oar_sum``
47+
``plan.output.fractions_used``
48+
49+
A full list of optional attributes (dependent if the user specifies plot):
50+
``plan.output.policy``
51+
``plan.output.value``
52+
``plan.output.remains``
53+
``plan.output.probability``
54+
55+
A full list of available functions:
56+
``plan.optimise()``
57+
``plan.plot()``
58+
``plan.fraction_counter()``
59+
60+
61+
Examples
62+
--------
63+
Consider the following demonstration:
64+
65+
>>> import adaptfx as afx
66+
>>> plan = afx.RL_object('path/to/instruction_file')
67+
68+
>>> plan.keys.sparing_factors
69+
[0.98, 0.97, 0.8, 0.83, 0.8, 0.85, 0.94]
70+
71+
72+
>>> plan.optimise()
73+
process duration: 0.0219 s:
74+
>>> plan.output.oar_doses
75+
array([ 0. , 3.2, 3.3, 11.6, 107.1])
76+
>>> plan.output.tumor_sum
77+
72.0
78+
>>> plan.output.fractions_used
79+
5
80+
81+
"""
82+
def __init__(self, instruction_filename_in):
83+
instruction_filename, basename = afx.get_abs_path(instruction_filename_in, nme)
1584
try: # check if file can be opened
1685
with open(instruction_filename, 'r') as f:
1786
read_in = f.read()
@@ -48,7 +117,7 @@ def __init__(self, instruction_filename):
48117
if not log_level in afx.LOG_LEVEL_LIST:
49118
afx.aft_error('invalid "debug" flag was set', nme)
50119

51-
afx.logging_init(instruction_filename, log_bool, log_level)
120+
afx.logging_init(basename, log_bool, log_level)
52121
afx.aft_message_info('log level:', log_level, nme)
53122
afx.aft_message_info('log to file:', log_bool, nme)
54123

@@ -86,14 +155,19 @@ def __init__(self, instruction_filename):
86155
settings = afx.setting_reader(afx.SETTING_DICT, user_settings)
87156
afx.aft_message_dict('settings', settings, nme, 1)
88157

158+
self.filename = instruction_filename
159+
self.basename = basename
89160
self.algorithm = algorithm
90161
self.log = log_bool
91162
self.log_level = log_level
92163
self.keys = afx.DotDict(keys)
93164
self.settings = afx.DotDict(settings)
94165

95166
def optimise(self):
167+
afx.aft_message('optimising...', nme, 1)
168+
start = afx.timing()
96169
self.output = afx.multiple(self.algorithm, self.keys, self.settings)
170+
afx.timing(start)
97171

98172
def fraction_counter(self):
99173
if self.algorithm == 'frac' and self.keys.fraction == 0:
@@ -104,14 +178,27 @@ def fraction_counter(self):
104178
def plot(self):
105179
out = self.output
106180
sets = self.settings
181+
figures = []
107182
if self.settings.plot_policy:
108-
afx.plot_val(out.policy.sf, out.policy.states, out.policy.val, out.policy.fractions)
183+
policy_fig = afx.plot_val(out.policy.sf, out.policy.states, out.policy.val,
184+
out.policy.fractions, 'turbo')
185+
figures.append(policy_fig)
109186
if self.settings.plot_values:
110-
afx.plot_val(out.value.sf, out.value.states, out.value.val, out.value.fractions)
187+
values_fig = afx.plot_val(out.value.sf, out.value.states,
188+
out.value.val, out.value.fractions, 'viridis')
189+
figures.append(values_fig)
111190
if self.settings.plot_remains:
112-
afx.plot_val(out.remains.sf, out.remains.states, out.remains.val, out.remains.fractions)
113-
114-
if sets.plot_policy or sets.plot_values or sets.plot_remains:
191+
remains_fig = afx.plot_val(out.remains.sf, out.remains.states,
192+
out.remains.val, out.remains.fractions, 'plasma')
193+
figures.append(remains_fig)
194+
if self.settings.plot_probability:
195+
prob_fig = afx.plot_probability(out.probability.sf, out.probability.pdf,
196+
out.probability.fractions)
197+
figures.append(prob_fig)
198+
199+
if sets.save_plot:
200+
afx.save_plot(self.basename, *figures)
201+
elif sets.plot_policy or sets.plot_values or sets.plot_remains or sets.plot_probability:
115202
afx.show_plot()
116203
else:
117204
afx.aft_message('nothing to plot', nme, 1)
@@ -120,7 +207,6 @@ def main():
120207
"""
121208
CLI interface to invoke the RL class
122209
"""
123-
start = afx.timing()
124210
parser = argparse.ArgumentParser(
125211
description='Calculate optimal dose per fraction dependent on algorithm type'
126212
)
@@ -143,9 +229,7 @@ def main():
143229
args = parser.parse_args(args=None if sys.argv[1:] else ['--help'])
144230
plan = RL_object(args.filename)
145231

146-
afx.aft_message('start session...', nme, 1)
147232
plan.optimise()
148-
afx.timing(start)
149233

150234
# show retrospective dose prescribtion
151235
afx.aft_message_list('physical tumor dose:', plan.output.physical_doses, nme, 1)
@@ -165,4 +249,4 @@ def main():
165249

166250

167251
if __name__ == '__main__':
168-
main()
252+
main()

0 commit comments

Comments
 (0)