Skip to content

Commit

Permalink
Merge pull request #10 from openAFT/dev
Browse files Browse the repository at this point in the history
dev
  • Loading branch information
janicweber authored Jun 20, 2023
2 parents cff9c99 + a6e283e commit 8212b34
Show file tree
Hide file tree
Showing 30 changed files with 1,601 additions and 386 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ cover/

# OS
.DS_Store
*.pdf

# Django stuff:
*.log
Expand Down
76 changes: 70 additions & 6 deletions MANUAL.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

## Working tree

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

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)

## Format of the instruction file
The user specifies following elements of the dictionary for the main entries:
Expand Down Expand Up @@ -94,7 +96,7 @@ min_dose : float
max_dose : float
maximal physical doses to be delivered in one fraction.
The doses are aimed at PTV 95. If -1 the dose is adapted to the
remaining dose tumor dose to be delivered or the remaining OAR dose
remaining tumor BED to be delivered or the remaining OAR dose
allowed to be prescribed.
default: -1
```
Expand Down Expand Up @@ -136,24 +138,86 @@ sf_high : float
sf_stepsize: float
stepsize of the sparing factor stepsize.
sf_prob_threshold': float
probability threshold of the sparing factor occuring.
probability threshold of the sparing factor occuring
which defines the range of sparing factors.
inf_penalty : float
infinite penalty for certain undesired states.
define infinite penalty for undesired states
choose arbitrarily large compared to highest occuring reward.
plot_policy : int
starting from which fraction policy should be plotted.
plot_values : int
starting from which fraction value should be plotted.
plot_remains : int
starting from which fraction expected remaining number
of fractions should be plotted.
plot_probability : int
flag if the probability distribution should be plotted
```

# Example
> :warning: Note:\
> 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`.
## Note on Plots
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.

Different is the probability plot: the probability is set for each fraction and updated with additional oberved sparing factors.

# AFT CLI

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

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

```
$ aft -f work/oar_example.json
```

# AST CLI
There is also a second CLI that allows to plot sparing factors, policy functions, temporal Adaptive Fractionation Therapy etc.
```
$ ast [options] -f <simulation_file>
```

The entry for algorithm simulation type is

```
algorithm_simulation: histogram, fraction, single_state, all_state
single_distance, single_patient, grid_distance, grid_fraction
allowed plots options
keys_simulation: dict
simulation keys
```

```
keys_simulation
----------------
# Histogram of applied AFT for sampled patients
n_patients : float
stepsize of the actionspace.
fixed_mean_sample : float
mean of sampled patient sparing factor
fixed_std_sample : float
standard deviation of sampled patient sparing factor
# Data related plots
c_list : list
list of c parameters
plot_index : int
which index to plot
data_filepath : string
path to sparing factor file
data_selection : list
two elements of header in sparing factor file
data_row_hue : string
seaborn hue or row
# Settings of plots
figsize : list
two elements of size figure
fontsize : float
fontsize of plots
save : bool
boolean to instruct saving plot
usetex : bool
boolean if TeX font should be used
```
14 changes: 9 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,10 @@ A last addition is made with graphical user interfaces that facilitate the use o
> :warning: Note:\
> The interfaces are not optimized, and thus it is not recommended using them to further develop extensions.
### Reducing Number of Fractions

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

### Probability Updating

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.
Expand Down Expand Up @@ -173,12 +177,12 @@ ImportError: No module named '_ctypes'
**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

```
sudo dnf install libffi-devel
$ sudo dnf install libffi-devel
```

On Ubuntu:
```
sudo apt install libffi-dev
$ sudo apt install libffi-dev
```

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

```
sudo dnf install python3-tkinter
$ sudo dnf install python3-tkinter
```

on Ubuntu

```
sudo apt-get install python3-tk
$ sudo apt-get install python3-tk
```

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

```
pip install pyqt5
$ pip install pyqt5
```


Expand Down
5 changes: 4 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@ dependencies = [
"numpy == 1.24.1",
"scipy == 1.10.0",
"matplotlib == 3.6.3",
"pyqt6 == 6.4.0"
"pyqt6 == 6.4.0",
"pandas==1.5.2",
"seaborn==0.12.2"
]
dynamic = ["version"]

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

[project.scripts]
aft = "adaptfx.aft:main"
ast = "adaptsim.ast:main"
2 changes: 1 addition & 1 deletion src/adaptfx/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from .radiobiology import *
from .visualiser import *
from .reinforce import *
from .reinforce_old import *
from .reinforce_old import max_tumor_bed_old, min_oar_bed_old, min_oar_max_tumor_old

__all__ = ['bed_calc_matrix',
'convert_to_physical',
Expand Down
114 changes: 99 additions & 15 deletions src/adaptfx/aft.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,80 @@

class RL_object():
"""
Reinforcement Learning class to check instructions
of calculation, invoke keys and define
calculation settings from file
"""
def __init__(self, instruction_filename):
Invokes a class with one dictionary object and multiple functions
to operate on the dictionary. Dictionary is created from instruction
file and contains instructions, keys, settings. Once optimisation
is performed it also provides results.
Parameters
----------
instruction_filename_in : string
string of instruction filename
Returns
-------
returns : ``plan`` class
The optimisation result represented as a ``plan`` object.
Important attributes are: ``keys`` and ``settings`` the keys and
settings from the instruction file. To start the optimisation one
can run ``plan.optimise`` which will store the results in ``output``.
Important attributes are: ``output.physical_doses``,
``output.tumor_doses``, ``output.oar_doses``.
If one is looking for summed doses they are found in:
``output.tumor_sum``, ``output.oar_sum``.
In case where number of fractions are minimised one can check
utilised number of fractions with ``output.fractions_used``.
A full list of attributes:
``plan.filename``
``plan.basename``
``plan.algorithm``
``plan.log``
``plan.log_level``
``plan.keys``
``plan.settings``
``plan.output.physical_doses``
``plan.output.tumor_doses``
``plan.output.oar_doses``
``plan.output.tumor_sum``
``plan.output.oar_sum``
``plan.output.fractions_used``
A full list of optional attributes (dependent if the user specifies plot):
``plan.output.policy``
``plan.output.value``
``plan.output.remains``
``plan.output.probability``
A full list of available functions:
``plan.optimise()``
``plan.plot()``
``plan.fraction_counter()``
Examples
--------
Consider the following demonstration:
>>> import adaptfx as afx
>>> plan = afx.RL_object('path/to/instruction_file')
>>> plan.keys.sparing_factors
[0.98, 0.97, 0.8, 0.83, 0.8, 0.85, 0.94]
>>> plan.optimise()
process duration: 0.0219 s:
>>> plan.output.oar_doses
array([ 0. , 3.2, 3.3, 11.6, 107.1])
>>> plan.output.tumor_sum
72.0
>>> plan.output.fractions_used
5
"""
def __init__(self, instruction_filename_in):
instruction_filename, basename = afx.get_abs_path(instruction_filename_in, nme)
try: # check if file can be opened
with open(instruction_filename, 'r') as f:
read_in = f.read()
Expand Down Expand Up @@ -48,7 +117,7 @@ def __init__(self, instruction_filename):
if not log_level in afx.LOG_LEVEL_LIST:
afx.aft_error('invalid "debug" flag was set', nme)

afx.logging_init(instruction_filename, log_bool, log_level)
afx.logging_init(basename, log_bool, log_level)
afx.aft_message_info('log level:', log_level, nme)
afx.aft_message_info('log to file:', log_bool, nme)

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

self.filename = instruction_filename
self.basename = basename
self.algorithm = algorithm
self.log = log_bool
self.log_level = log_level
self.keys = afx.DotDict(keys)
self.settings = afx.DotDict(settings)

def optimise(self):
afx.aft_message('optimising...', nme, 1)
start = afx.timing()
self.output = afx.multiple(self.algorithm, self.keys, self.settings)
afx.timing(start)

def fraction_counter(self):
if self.algorithm == 'frac' and self.keys.fraction == 0:
Expand All @@ -104,14 +178,27 @@ def fraction_counter(self):
def plot(self):
out = self.output
sets = self.settings
figures = []
if self.settings.plot_policy:
afx.plot_val(out.policy.sf, out.policy.states, out.policy.val, out.policy.fractions)
policy_fig = afx.plot_val(out.policy.sf, out.policy.states, out.policy.val,
out.policy.fractions, 'turbo')
figures.append(policy_fig)
if self.settings.plot_values:
afx.plot_val(out.value.sf, out.value.states, out.value.val, out.value.fractions)
values_fig = afx.plot_val(out.value.sf, out.value.states,
out.value.val, out.value.fractions, 'viridis')
figures.append(values_fig)
if self.settings.plot_remains:
afx.plot_val(out.remains.sf, out.remains.states, out.remains.val, out.remains.fractions)

if sets.plot_policy or sets.plot_values or sets.plot_remains:
remains_fig = afx.plot_val(out.remains.sf, out.remains.states,
out.remains.val, out.remains.fractions, 'plasma')
figures.append(remains_fig)
if self.settings.plot_probability:
prob_fig = afx.plot_probability(out.probability.sf, out.probability.pdf,
out.probability.fractions)
figures.append(prob_fig)

if sets.save_plot:
afx.save_plot(self.basename, *figures)
elif sets.plot_policy or sets.plot_values or sets.plot_remains or sets.plot_probability:
afx.show_plot()
else:
afx.aft_message('nothing to plot', nme, 1)
Expand All @@ -120,7 +207,6 @@ def main():
"""
CLI interface to invoke the RL class
"""
start = afx.timing()
parser = argparse.ArgumentParser(
description='Calculate optimal dose per fraction dependent on algorithm type'
)
Expand All @@ -143,9 +229,7 @@ def main():
args = parser.parse_args(args=None if sys.argv[1:] else ['--help'])
plan = RL_object(args.filename)

afx.aft_message('start session...', nme, 1)
plan.optimise()
afx.timing(start)

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


if __name__ == '__main__':
main()
main()
Loading

0 comments on commit 8212b34

Please sign in to comment.