Plugins
Warning
work in progress
Plugins offer a way to exchange the behaviour of some part of LBFextract’s workflow. This is achieved through a hook mechanism. LBFextract workflow for a feature extraction method is described in the core module and is composed by a sequence of steps. Each step is a hook. Hooks are implemented by plugins. The default behaviour of LBFextract is itself implemented in the fextract plugin, which provides the implementation of the coverage-based methods. Each hook is essentially a decorated python function. In LBFextract there are two types of hooks: CliHook and FextractHooks. The former is responsible for the implementation of the different steps of the workflow. The latter is responsible for the CLI commands needed to run the feature extraction method. The implementation in LBFextract is simplified through the “setup new-plugin” command. i.e. to create a new plugin extracting a “cool_signal_name” (the complete implementation of this example can be found here), one can run the following:
lbfextract setup new-plugin --name_of_the_signal cool_signal_name --name_of_the_cli_command extract_cool_signal_name --out_dir OUTPUTDIR
This will generate most of the boilerplate code needed to generate a LBFextract’s pluing. An LBFextract’s plugin includes the following files:
fextract-cool_signal_name
├── setup.py
├── src
│ ├── cool_signal_name
│ │ ├── __init__.py
│ └── └── plugin.py
└── tests
Once it has been generated, the plugin can be installed as follow:
cd OUTPUTDIR && python -m pip install .
after a successful installation, the extract_cool_signal_name command will be visible in the lbfextract feature_extraction_commands –help. At this moment the package won’t do much since the hooks are not implemented. To implement them one can open the generated plugin.py file. Inside, there is the definition of all hooks that can be implemented and the default CLI command. By default the CLI command is equal to the extract_coverage command, because this is the original definition of the hooks. Depending on the hooks that are implemented by the plugin, the CLI command has to be updated.
After generating the plugin the plugin.py file will look like this:
1import logging
2import pathlib
3from typing import Any
4from typing import List
5from typing import Optional
6
7import click
8import matplotlib
9import pandas as pd
10from lbfextract.utils_classes import Signal
11
12import lbfextract.fextract
13from lbfextract.core import App
14from lbfextract.fextract.schemas import Config, AppExtraConfig, ReadFetcherConfig, SingleSignalTransformerConfig, \
15 SignalSummarizer
16
17logger = logging.getLogger(__name__)
18
19class FextractHooks:
20
21 @lbfextract.hookimpl
22 def fetch_reads(self,
23 path_to_bam: pathlib.Path,
24 path_to_bed: pathlib.Path,
25 config: Any,
26 extra_config: Any) -> pd.DataFrame:
27 """
28 :param path_to_bam: path to the bam file
29 :param path_to_bed: path to the bed file with the regions to be filtered
30 :param config: configuration file containing the configuration object required by the fetch_reads function
31 :param extra_config: extra configuration that may be used in the hook implementation
32 :return: ReadsPerIntervalContainer object containing all the ReadsPerInterval objects in all the intervals
33 contained in the bed file
34 """
35 return None
36
37 @lbfextract.hookimpl
38 def save_fatched_reads(self, reads_per_interval_container: pd.DataFrame,
39 config: Any,
40 extra_config: Any
41 ) -> None:
42 """
43 Hook implementing the strategy to save the reads fetched from the intervals
44 :param reads_per_interval_container: ReadsPerIntervalContainer containing information about the genomic region
45 and the reads mapping to it
46 :param output_path: path to the location where the data should be stored
47 :param id: run id
48 :param time_stamp: time stamp
49 :param extra_config: extra configuration that may be used in the hook implementation
50
51 :return: None
52 """
53 return None
54
55 @lbfextract.hookimpl
56 def load_fetched_reads(self, config: Any, extra_config: AppExtraConfig) -> pd.DataFrame:
57 """
58 :param config: config specific to the function
59 :param extra_config: extra configuration that may be used in the hook implementation
60 """
61 return None
62
63 @lbfextract.hookimpl
64 def transform_reads(self, reads_per_interval_container: pd.DataFrame, config: Any,
65 extra_config: Any) -> pd.DataFrame:
66 """
67 :param reads_per_interval_container: ReadsPerIntervalContainer containing a list of ReadsPerInterval which are
68 basically lists with information about start and end of the interval
69 :param config: config specific to the function
70 :param extra_config: extra configuration that may be used in the hook implementation
71 """
72 return None
73
74 @lbfextract.hookimpl
75 def transform_single_intervals(self, transformed_reads: pd.DataFrame, config: Any,
76 extra_config: Any) -> Signal:
77 """
78 :param transformed_reads: ReadsPerIntervalContainer containing a list of ReadsPerInterval which are
79 basically lists with information about start and end of the interval
80 :param config: config specific to the function
81 :param extra_config: config containing context information plus extra parameters
82 """
83 return None
84
85 @lbfextract.hookimpl
86 def transform_all_intervals(self, single_intervals_transformed_reads: Signal, config: Any,
87 extra_config: Any) -> Signal:
88 """
89 :param single_intervals_transformed_reads: Signal object containing the signals per interval
90 :param config: config specific to the function
91 :param extra_config: extra configuration that may be used in the hook implementation
92 """
93 return None
94
95 @lbfextract.hookimpl
96 def plot_signal(self, signal: Signal, config: Any, extra_config: Any) -> matplotlib.figure.Figure:
97 """
98 :param signal: Signal object containing the signals per interval
99 :param extra_config: extra configuration that may be used in the hook implementation
100 """
101 return None
102
103 @lbfextract.hookimpl
104 def save_signal(self,
105 signal: Signal,
106 config: Any,
107 extra_config: Any) -> None:
108 """
109 :param signal: Signal object containing the signals per interval
110 :param extra_config: extra configuration that may be used in the hook implementation
111 """
112
113 return None
114
115
116class CliHook:
117 @lbfextract.hookimpl_cli
118 def get_command(self) -> click.Command | List[click.Command]:
119
120 @click.command()
121 @click.option('--path_to_bam', type=click.Path(exists=False,
122 file_okay=True,
123 dir_okay=True,
124 writable=False,
125 readable=True,
126 resolve_path=False,
127 allow_dash=True,
128 path_type=pathlib.Path,
129 executable=False),
130 help='path to the bam file to be used')
131 @click.option('--path_to_bed', type=click.Path(exists=False,
132 file_okay=True,
133 dir_okay=True,
134 writable=False,
135 readable=True,
136 resolve_path=False,
137 allow_dash=True,
138 path_type=pathlib.Path,
139 executable=False),
140 help='path to the bed file to be used')
141 @click.option('--output_path', type=click.Path(exists=False,
142 file_okay=False,
143 dir_okay=True,
144 writable=True,
145 readable=True,
146 resolve_path=False,
147 allow_dash=True,
148 path_type=pathlib.Path,
149 executable=False),
150 help='path to the output directory')
151 @click.option("--skip_read_fetching", is_flag=True, show_default=True,
152 help='Boolean flag. When it is set, the fetching of the reads is skipped and the latest'
153 'timestamp of this run (identified by the id) is retrieved')
154 @click.option("--exp_id", default=None, type=str, show_default=True,
155 help="run id")
156 @click.option("--window", default=1000, type=int, show_default=True,
157 help="Integer describing the number of bases to be extracted around the middle point of an"
158 " interval present in the bedfile")
159 @click.option("--flanking_window", default=1000, type=int, show_default=True,
160 help="Integer describing the number of bases to be extracted after the window")
161 @click.option("--extra_bases", default=2000, type=int, show_default=True,
162 help="Integer describing the number of bases to be extracted from the bamfile when removing the "
163 "unused bases to be sure to get all the proper paires, which may be mapping up to 2000 bs")
164 @click.option("--n_binding_sites", default=1000, type=int, show_default=True,
165 help="number of intervals to be used to extract the signal, if it is higher then the provided"
166 "intervals, all the intervals will be used")
167 @click.option("--summarization_method", default="mean",
168 type=click.Choice(["mean", "median", "max", "min", "skip"]),
169 show_default=True,
170 help=f"method to be used to summarize the signal: (Undefined, Undefined, Undefined, Undefined)")
171 @click.option("--cores", default=1, type=int, show_default=True,
172 help="number of cores to be used")
173 @click.option("--flip_based_on_strand", is_flag=True,
174 show_default=True,
175 default=False,
176 help="flip the signal based on the strand")
177 @click.option('--gc_correction_tag', type=str,
178 default=None, help='tag to be used to extract gc coefficient per read from a bam file')
179
180 # Here you can add the options you want to be available in the command line (they should match the ones in the
181 # function)
182 def extract_cool_signal_name(
183 path_to_bam: pathlib.Path, path_to_bed: pathlib.Path,
184 output_path: pathlib.Path,
185 skip_read_fetching,
186 window: int,
187 flanking_window: int,
188 extra_bases: int,
189 n_binding_sites: int,
190 summarization_method: str,
191 cores: int,
192 exp_id: Optional[str],
193 flip_based_on_strand: bool,
194 gc_correction_tag: Optional[str]
195 ):
196 """
197 here yuo can add the code that will be executed when the command is called.
198 You can use the following code to customize the hooks
199 each hook receives a dictionary of configuration parameters
200 if the hook is implemented in the plugin, the parameters should be added in the correct hook.
201 Here you can find the parameters of the default hook (extract_coverage). These can be modified
202 depending on which hook you are using.
203 Hook specified above will be used in place of the default one. The first hook with a non None return value
204 will be used. Therfore the order of the plugins specified in the plugins_name list is important.
205 If you are inheriting from multiple signals, be sure to pass the correct parameters to the correct hooks.
206 Generally, parameters to be pass to hooks are shown in the schemas.py file of each plugin.
207 If you define a new hook for best practice you could inherit from lbfextract.fextract.schemas and specify
208 the voluptuous schema for the parameters of the hook.
209 """
210 read_fetcher_config = {
211 "window": window,
212 "flanking_region_window": flanking_window,
213 "extra_bases": extra_bases,
214 "n_binding_sites": n_binding_sites
215 }
216 reads_transformer_config = {}
217 single_signal_transformer_config = {
218 "signal_transformer": "coverage",
219 "flip_based_on_strand": flip_based_on_strand,
220 "gc_correction": True if gc_correction_tag else False,
221 "tag": gc_correction_tag
222
223 }
224 transform_all_intervals_config = {
225 "summarization_method": summarization_method
226 }
227 plot_signal_config = {}
228 save_signal_config = {}
229 extra_config = {
230 "cores": cores
231 }
232 output_path.mkdir(parents=True, exist_ok=True)
233 output_path_interval_spec = output_path / f"{path_to_bam.stem}" / f"{path_to_bed.stem}"
234 output_path_interval_spec.mkdir(parents=True, exist_ok=True)
235 res = App(plugins_name=["cool_signal_name", ],
236 path_to_bam=path_to_bam,
237 path_to_bed=path_to_bed,
238 output_path=output_path_interval_spec or pathlib.Path.cwd(),
239 skip_read_fetching=skip_read_fetching,
240 read_fetcher_config=ReadFetcherConfig(read_fetcher_config),
241 reads_transformer_config=Config(reads_transformer_config),
242 single_signal_transformer_config=SingleSignalTransformerConfig(single_signal_transformer_config),
243 transform_all_intervals_config=SignalSummarizer(transform_all_intervals_config),
244 plot_signal_config=Config(plot_signal_config),
245 save_signal_config=Config(save_signal_config),
246 extra_config=AppExtraConfig(extra_config),
247 id=exp_id).run()
248 return res
249
250 return extract_cool_signal_name
251
252hook = FextractHooks()
253hook_cli = CliHook()
Assuming the cool signal is just the number of reads in each interval, one could implement this by replacing the transform_single_intervals and transform_all_intervals hooks as follow:
1@lbfextract.hookimpl
2def transform_single_intervals(self, transformed_reads: pd.DataFrame, config: Any,
3 extra_config: Any) -> Signal:
4 """
5 :param transformed_reads: ReadsPerIntervalContainer containing a list of ReadsPerInterval which are
6 basically lists with information about start and end of the interval
7 :param config: config specific to the function
8 :param extra_config: config containing context information plus extra parameters
9 """
10 # transformed_reads is a pandas DataFrame with the following columns:
11 # 'Chromosome', 'Start', 'End', 'Name', 'Score', 'Strand', 'reads_per_interval'
12 # and reads_per_interval contains pysam.libcalignmentfile.IteratorRowRegion for each interval
13
14 def count_rads_in_interval(x: pysam.libcalignmentfile.IteratorRowRegion) -> int:
15 return len(list(x))
16
17 transformed_reads.index = transformed_reads.Chromosome.astype("str") + "-" + transformed_reads.Start.astype(
18 "str") + "-" + transformed_reads.End.astype("str")
19
20 reads_per_interval_pd_series = transformed_reads.reads_per_interval.apply(lambda x: count_rads_in_interval(x))
21
22 return Signal(reads_per_interval_pd_series.values,
23 metadata=reads_per_interval_pd_series.index.copy(),
24 tags=tuple(["cool_signal", ]))
25
26@lbfextract.hookimpl
27def transform_all_intervals(self, single_intervals_transformed_reads: Signal, config: Any,
28 extra_config: Any) -> Signal:
29 """
30 :param single_intervals_transformed_reads: Signal object containing the signals per interval
31 :param config: config specific to the function
32 :param extra_config: extra configuration that may be used in the hook implementation
33 """
34 return single_intervals_transformed_reads
In this way when executing:
lbfextract feature_extraction_commands extract-cool-signal-name --path_to_bam <path_to_bam> --path_to_bed <path_to_bed> --output_path <output_path>
after fetching the reads, lbfextract will execute the newly implemented hooks in place of the default ones. One can use already implemented hooks in other plugins by registering them in the App.plugins_name attribute. This accepts a list of plugins. Hooks are parsed from left to right, and only the first not None hook will be run. i.e. if there are 2 plugins implementing the plot_signal hook, which are registered in the App object as follow: App.plugins_name([“plug2”, “plug1”]), only plug2 plot_signal hook will be run. New parameters can be provided in form of config. Config can be dict object but LBFextract provides also a Config object, which offers a way of validating the provided parameters with a voluptuous schema. One can subclass the Config class providing a voluptuous schema. One can inspect the lbfextract.fextract.schema module for an example.