File Coverage

utils/bff_pxf_simulator/bff-pxf-simulator
Criterion Covered Total %
statement 167 175 95.4
branch 32 42 76.1
condition 6 17 35.2
subroutine 39 41 95.1
pod n/a
total 244 275 88.7


line stmt bran cond sub pod time code
1             #!/usr/bin/env perl
2             #
3             # A script that creates a JSON array of simulated BFF/PXF
4             #
5             # Note: Check also Monarch Initiative:
6             # https://github.com/monarch-initiative/PhenoImp
7             #
8             # Last Modified: Nov/08/2024
9             #
10             # $VERSION taken from Pheno::Ranker
11             #
12             # Copyright (C) 2023-2024 Manuel Rueda - CNAG (manuel.rueda@cnag.eu)
13             #
14             # License: Artistic License 2.0
15             #
16             # If this program helps you in your research, please cite.
17              
18 3     3   11854 use strict;
  3         5  
  3         123  
19 3     3   28 use warnings;
  3         4  
  3         141  
20 3     3   1744 use Getopt::Long qw(:config no_ignore_case);
  3         35499  
  3         14  
21 3     3   1717 use Pod::Usage;
  3         166434  
  3         1027  
22              
23             ##### Main #####
24 3         357752 randomize_ga4gh();
25             ################
26 3         295 exit;
27              
28             sub randomize_ga4gh {
29              
30 3     3   10 my $VERSION = '0.09';
31 3         7 my $format = 'bff';
32 3         5 my $number = 100;
33 3         10 my $output = 'individuals.json';
34 3         13 my ( $phenotypicFeatures, $diseases, $treatments, $procedures, $exposures,
35             $ethnicity )
36             = (1) x 6;
37              
38             # Reading arguments
39             GetOptions(
40             'format|f=s' => \$format, # string
41             'number|n=i' => \$number, # string
42             'output|o=s' => \$output, # string
43              
44             #
45             'diseases=i' => \$diseases, # integer
46             'exposures=i' => \$exposures, # integer
47             'phenotypicFeatures=i' => \$phenotypicFeatures, # integer
48             'procedures=i' => \$procedures, # integer
49             'treatments=i' => \$treatments, # integer
50              
51             #
52             'max-diseases-pool=i' => \my $max_diseases_pool, # integer
53             'max-ethnicity-pool=i' => \my $max_ethnicity_pool, # integer
54             'max-exposures-pool=i' => \my $max_exposures_pool, # integer
55             'max-phenotypicFeatures-pool=i' => \my $max_phenotypicFeatures_pool, # integer
56             'max-treatments-pool=i' => \my $max_treatments_pool, # integer
57             'max-procedures-pool=i' => \my $max_procedures_pool, # integer
58              
59             #
60             'random-seed=i' => \my $random_seed, # integer
61             'external-ontologies=s' => \my $ext_ontologies, # string
62              
63             #
64             'help|?' => \my $help, # flag
65             'man' => \my $man, # flag
66             'debug=i' => \my $debug, # integer
67             'verbose|' => \my $verbose, # flag
68 0     0   0 'version|V' => sub { print "$0 Version $VERSION\n"; exit; }
  0         0  
69 3 50       45 ) or pod2usage(2);
70 3 50       7512 pod2usage(1) if $help;
71 3 50       7 pod2usage( -verbose => 2, -exitval => 0 ) if $man;
72              
73             # Create object
74 3         58 my $randomize = Randomizer->new(
75             {
76             format => $format,
77             number => $number,
78             output => $output,
79             diseases => $diseases,
80             ethnicity => $ethnicity,
81             exposures => $exposures,
82             phenotypicFeatures => $phenotypicFeatures,
83             procedures => $procedures,
84             treatments => $treatments,
85             max_diseases_pool => $max_diseases_pool,
86             max_ethnicity_pool => $max_ethnicity_pool,
87             max_exposures_pool => $max_exposures_pool,
88             max_phenotypicFeatures_pool => $max_phenotypicFeatures_pool,
89             max_procedures_pool => $max_procedures_pool,
90             max_treatments_pool => $max_treatments_pool,
91             random_seed => $random_seed,
92             ext_ontologies => $ext_ontologies,
93             debug => $debug,
94             verbose => $verbose
95             }
96             );
97              
98             # Run method
99 3         13 $randomize->run;
100             }
101              
102             package Randomizer;
103              
104 3     3   18 use strict;
  3         5  
  3         50  
105 3     3   9 use warnings;
  3         5  
  3         99  
106 3     3   1205 use autodie;
  3         39483  
  3         11  
107 3     3   13731 use feature qw(say);
  3         18  
  3         383  
108              
109             #use Data::Printer;
110 3     3   1672 use Data::Dumper;
  3         19143  
  3         197  
111 3     3   2328 use Path::Tiny;
  3         32683  
  3         224  
112 3     3   23 use List::Util 1.50 qw(head shuffle);
  3         66  
  3         305  
113 3     3   2290 use JSON::XS;
  3         13573  
  3         224  
114 3     3   1191 use Data::Fake qw(Core Company Dates Names);
  3         8966  
  3         29  
115 3     3   52995 use FindBin qw($Bin);
  3         3294  
  3         380  
116 3     3   1190 use lib $Bin;
  3         1916  
  3         20  
117             use Ontologies
118 3     3   2251 qw($hpo_array $omim_array $rxnorm_array $ncit_procedures_array $ncit_exposures_array $ethnicity_array);
  3         35  
  3         5866  
119              
120             sub new {
121 3     3   7 my ( $class, $self ) = @_;
122 3         6 bless $self, $class;
123 3         7 return $self;
124             }
125              
126             sub run {
127              
128 3     3   6 my $self = shift;
129 3         16 my $number = $self->{number};
130 3         6 my $format = $self->{format};
131 3         6 my $output = $self->{output};
132 3         5 my $random_seed = $self->{random_seed};
133 3         12 my %func = (
134             bff => \&bff_generator,
135             pxf => \&pxf_generator
136             );
137              
138             # Set seed if defined
139 3 50       52 srand($random_seed) if defined $random_seed; # user can set it to 0
140              
141             # Load external ontologies file if present
142             $self->{ontologies_data} =
143             $self->{ext_ontologies}
144             ? validate_json( $self->{ext_ontologies} )
145 3 100       15 : undef; # setter
146              
147             #########
148             # START #
149             #########
150              
151 3         6 my $json_data;
152 3         11 for ( my $i = 1 ; $i <= $number ; $i++ ) {
153 300         368063 push @$json_data, $func{$format}->( $i, $self );
154             }
155              
156             #######
157             # END #
158             #######
159             #p $json_data;
160              
161             # Serialize the data and write
162 3         3751 write_json( { filepath => $output, data => $json_data } );
163             }
164              
165             sub write_json {
166              
167 3     3   6 my $arg = shift;
168 3         7 my $file = $arg->{filepath};
169 3         5 my $json_data = $arg->{data};
170              
171             # Note that canonical DOES not match the order of nsort from Sort::Naturally
172 3         22465 my $json = JSON::XS->new->utf8->canonical->pretty->encode($json_data);
173 3         57 path($file)->spew_utf8($json);
174 3         37258 return 1;
175             }
176              
177             sub pxf_generator {
178              
179 100     100   131 my ( $id, $self ) = @_;
180 100         137 my $result_hash = run_functions($self);
181             my $pxf = fake_hash(
182             {
183             id => "Phenopacket_" . $id,
184             subject => {
185             id => "IndividualId_" . $id,
186             age => {
187             iso8601duration =>
188             fake_template( "P%dY", fake_int_mod( 1, 99 ) )
189             },
190             sex => fake_pick_mod( [ 'MALE', 'FEMALE' ] )
191             },
192             diseases => $result_hash->{diseases},
193             phenotypicFeatures => $result_hash->{phenotypicFeatures},
194 100         222 medicalActions => merge_medical_actions($result_hash)
195             }
196             );
197 100         622 return $pxf->();
198             }
199              
200             sub merge_medical_actions {
201              
202 100     100   85 my $hash = shift;
203              
204             # Initialize empty arrays for treatments and procedures
205 100         87 my @processed_treatments;
206             my @processed_procedures;
207              
208             # Process treatments if defined
209 100 50       141 if ( defined $hash->{treatments} ) {
210             @processed_treatments =
211 100         81 map { { treatment => $_ } } @{ $hash->{treatments} };
  1000         1200  
  100         154  
212             }
213              
214             # Process procedures if defined
215 100 50       133 if ( defined $hash->{procedures} ) {
216             @processed_procedures =
217 100         82 map { { procedure => $_ } } @{ $hash->{procedures} };
  1000         1136  
  100         109  
218             }
219              
220             # Merge the processed arrays and return a reference
221             # NB: If undef no elements will be added
222 100         438 return [ @processed_treatments, @processed_procedures ];
223             }
224              
225             sub bff_generator {
226              
227 200     200   322 my ( $id, $self ) = @_;
228 200         204 my $default_array = [];
229 200         292 my $result_hash = run_functions($self);
230             my $bff = fake_hash(
231             {
232             id => "Beacon_" . $id,
233             ethnicity => $result_hash->{ethnicity},
234             sex => fake_pick_mod(
235             [
236             { id => "NCIT:C20197", label => "Male" },
237             { id => "NCIT:C16576", label => "Female" }
238             ]
239             ),
240             diseases => $result_hash->{diseases} // $default_array,
241             phenotypicFeatures => $result_hash->{phenotypicFeatures}
242             // $default_array,
243             treatments => $result_hash->{treatments} // $default_array,
244             interventionsOrProcedures => $result_hash->{procedures}
245             // $default_array,
246 200   33     725 exposures => $result_hash->{exposures} // $default_array
      33        
      33        
      66        
      33        
247             }
248             );
249 200         2669 return $bff->();
250             }
251              
252             ######################
253             # START ARRAY TERMS #
254             ######################
255              
256             sub create_entries {
257              
258 1500     1500   1680 my ( $params, $ontologies_array, $n, $max ) = @_;
259 1500         1556 my $shuffled_slice = shuffle_slice( $max, $ontologies_array );
260 1500         1266 my $array;
261 1500         1904 for ( my $i = 0 ; $i < $n ; $i++ ) {
262             push @$array,
263 9400         37039 $params->{entry_creator}->( $shuffled_slice->[$i], $params );
264             }
265 1500         8241 return $array;
266             }
267              
268             sub common_entry_creator {
269              
270 7300     7300   6789 my ( $element, $params ) = @_;
271             return {
272             $params->{type} => $element,
273             $params->{onset} => {
274 7300         8414 age => {
275             iso8601duration =>
276             fake_template( "P%dY", fake_int_mod( 1, 99 ) )
277             }
278             }
279             };
280             }
281              
282             sub phenotypicFeatures {
283              
284 300     300   366 my ( $format, $ontologies_array, $n, $max ) = @_;
285 300 100       727 my $params = {
    100          
286             type => $format eq 'bff' ? 'featureType' : 'type',
287             onset => $format eq 'bff' ? 'ageOfOnset' : 'onset',
288             entry_creator => \&common_entry_creator
289             };
290 300         447 return create_entries( $params, $ontologies_array, $n, $max );
291             }
292              
293             sub diseases {
294              
295 300     300   414 my ( $format, $ontologies_array, $n, $max ) = @_;
296 300 100       841 my $params = {
    100          
297             type => $format eq 'bff' ? 'diseaseCode' : 'term',
298             onset => $format eq 'bff' ? 'ageOfOnset' : 'onset',
299             entry_creator => \&common_entry_creator
300             };
301 300         458 return create_entries( $params, $ontologies_array, $n, $max );
302             }
303              
304             sub treatments {
305              
306 300     300   387 my ( $format, $ontologies_array, $n, $max ) = @_;
307             my $params = {
308             entry_creator => sub {
309 2100     2100   1878 my ( $element, $p ) = @_;
310 2100 100       3915 return $format eq 'bff'
311             ? { treatmentCode => $element }
312             : { agent => $element };
313             }
314 300         848 };
315 300         348 return create_entries( $params, $ontologies_array, $n, $max );
316             }
317              
318             sub procedures {
319              
320 300     300   377 my ( $format, $ontologies_array, $n, $max ) = @_;
321 300 100       752 my $params = {
    100          
322             type => $format eq 'bff' ? 'procedureCode' : 'term',
323             onset => $format eq 'bff' ? 'ageAtProcedure' : 'onset',
324             entry_creator => \&common_entry_creator
325             };
326 300         368 return create_entries( $params, $ontologies_array, $n, $max );
327             }
328              
329             sub exposures {
330              
331 300     300   372 my ( $format, $ontologies_array, $n, $max ) = @_;
332 300         276 my $default_duration = 'P999Y';
333 300         489 my $default_ontology_term =
334             { id => 'NCIT:C126101', label => 'Not Available' };
335 300 100       808 my $params = {
    100          
336             type => $format eq 'bff' ? 'exposureCode' : 'term',
337             onset => $format eq 'bff' ? 'ageAtExposure' : 'onset',
338             entry_creator => \&common_entry_creator
339             };
340 300         410 my $entries = create_entries( $params, $ontologies_array, $n, $max );
341              
342             # Add 'duration' 'and 'unit' to pass bff-validator
343 300         364 foreach my $entry (@$entries) {
344 1100         1007 $entry->{duration} = $default_duration;
345 1100         1056 $entry->{unit} = $default_ontology_term;
346             }
347 300         590 return $entries;
348             }
349              
350             sub ethnicity {
351              
352 300     300   418 my ( undef, $ontologies_array, undef, $max ) = @_;
353 300         357 my $shuffled_slice = shuffle_slice( $max, $ontologies_array );
354 300         499 return $shuffled_slice->[0];
355             }
356              
357             ####################
358             # END ARRAY TERMS #
359             ####################
360              
361             sub load_ontology_hash {
362              
363 300     300   302 my $self = shift;
364 300         959 my %ont = (
365             diseases => $omim_array,
366             ethnicity => $ethnicity_array,
367             exposures => $ncit_exposures_array,
368             phenotypicFeatures => $hpo_array,
369             procedures => $ncit_procedures_array,
370             treatments => $rxnorm_array
371             );
372 300         409 return \%ont;
373             }
374              
375             sub run_functions {
376              
377 300     300   318 my $self = shift;
378 300         398 my $ontologies = load_ontology_hash($self);
379              
380 300         749 my %func = (
381             diseases => \&diseases,
382             ethnicity => \ðnicity,
383             exposures => \&exposures,
384             phenotypicFeatures => \&phenotypicFeatures,
385             procedures => \&procedures,
386             treatments => \&treatments
387             );
388              
389 300         321 my %hash;
390              
391             # *** IMPORTANT ***
392             # sort keys (below) is mandatory for reproducibility
393 300         967 for my $key ( sort keys %func ) {
394             my $ontologies_array =
395             exists $self->{ontologies_data}{$key}
396             ? $self->{ontologies_data}{$key}
397 1800 100       2643 : $ontologies->{$key};
398             $hash{$key} = $func{$key}->(
399             $self->{format}, $ontologies_array, $self->{$key},
400 1800         3266 $self->{ 'max_' . $key . '_pool' }
401             );
402             }
403              
404 300         848 return \%hash;
405             }
406              
407             sub shuffle_slice {
408              
409 1800     1800   1746 my ( $max, $array ) = @_;
410              
411             # head -> 1.50 List::Util (5.26 has 1.4602)
412             #my @items = sample $count, @values; # 1.54 List::Util
413             # *** IMPORTANT ***
414             # If $max was defined by the user then use it, otherwise @$array;
415 1800 100       4042 my @slice = defined $max ? head $max, @$array : @$array; # slice of refs
416 1800         4337 my @shuffled_slice = shuffle @slice;
417 1800 50       2798 return wantarray ? @shuffled_slice : \@shuffled_slice;
418             }
419              
420             sub fake_int_mod {
421              
422             # This subroutine was built because fake_int did not respond to srand
423 7400     7400   6597 my ( $low, $high ) = @_;
424 7400         5805 my $range = $high - $low;
425 7400         9278 return int( rand($range) ) + 1;
426             }
427              
428             sub fake_pick_mod {
429              
430             # This subroutine was built because fake_pick did not respond to srand
431             # NB: The original from Data::Fake worked with array (not with arrayref)
432 300     300   693 my $array = shift;
433 300         1832 return $array->[ int( rand(@$array) ) ];
434             }
435              
436             sub validate_json {
437              
438 1     1   1 my $file = shift;
439 1         3 my $data = read_yaml($file);
440 1         226 my $schema = {
441             '$schema' => 'http://json-schema.org/draft-07/schema#',
442             type => "object",
443             properties => {
444             diseases => { '$ref' => '#/$defs/array' },
445             phenotypicFeatures => { '$ref' => '#/$defs/array' },
446             treatments => { '$ref' => '#/$defs/array' },
447             procedures => { '$ref' => '#/$defs/array' },
448             exposures => { '$ref' => '#/$defs/array' },
449             ethnicity => { '$ref' => '#/$defs/array' }
450             },
451             '$defs' => {
452             array => {
453             type => "array",
454             items => { '$ref' => '#/$defs/item' }
455             },
456             item => {
457             type => "object",
458             required => [ "id", "label" ],
459             properties => {
460             id => { type => "string", pattern => qq/^\\w[^:]+:.+\$/ },
461             label => { type => "string" }
462             }
463             }
464             }
465             };
466              
467             # Load at runtime
468 1         380 require JSON::Validator;
469              
470             # Create object and load schema
471 1         346965 my $jv = JSON::Validator->new;
472              
473             # Load schema in object
474 1         18 $jv->schema($schema);
475              
476             # Validate data
477 1         12979 my @errors = $jv->validate($data);
478              
479             # Show error if any
480 1 50 0     3648 say_errors( \@errors ) and die if @errors;
481              
482             # return data if ok
483 1         26 return $data;
484              
485             }
486              
487             sub say_errors {
488              
489 0     0   0 my $errors = shift;
490 0 0       0 if ( @{$errors} ) {
  0         0  
491 0         0 say join "\n", @{$errors};
  0         0  
492             }
493 0         0 return 1;
494             }
495              
496             sub read_yaml {
497              
498             # Load at runtime
499 1     1   547 require YAML::XS;
500 1         3805 YAML::XS->import('LoadFile');
501 1         4 return LoadFile(shift); # Decode to Perl data structure
502             }
503              
504             1;
505              
506             =head1 NAME
507              
508             bff-pxf-simulator: A script that creates a JSON array of simulated BFF/PXF
509              
510             =head1 SYNOPSIS
511              
512             bff-pxf-simulator [-options]
513              
514             Options:
515             -f, --format Format [bff|pxf]
516             -n, --number Set the number of individuals to generate [100]
517             -o, --output Output file [individuals.json]
518             --external-ontologies Path to a YAML file containing ontology terms
519             --random-seed Initializes pseudorandom number sequences (seed must be an integer)
520              
521             --diseases Set the number of diseases per individual [1]
522             --exposures Set the number of exposures per individual [1]
523             --phenotypicFeatures Set the number of phenotypic features per individual [1]
524             --procedures Set the number of procedures per individual [1]
525             --treatments Set the number of treatments per individual [1]
526             --max-[term]-pool Limit the selection to the first N elements of the term array
527             --max-ethnicity-pool Restrict the ethnicity pool size; each individual will have only one ethnicity
528              
529             Generic Options:
530             -debug Print debugging (from 1 to 5, being 5 max)
531             -h, --help Brief help message
532             -man Full documentation
533             -v, --verbose Verbosity on
534             -V, --version Print version
535              
536             =head1 DESCRIPTION
537              
538             This script generates a JSON array of simulated BFF/PXF data. The files can be created based on pre-loaded ontologies or by utilizing an external YAML file.
539              
540             =head1 SUMMARY
541              
542             A script that creates a JSON array of simulated BFF/PXF.
543              
544             Implemented array terms:
545              
546             B C.
547              
548             procedures = interventionsOrProcedures
549              
550             B C.
551              
552             procedures = medicalActions.procedure
553              
554             treatments = medicalActions.treatment
555              
556             =head1 INSTALLATION
557              
558             (only needed if you did not install C)
559              
560             $ cpanm --installdeps .
561              
562             =head3 System requirements
563              
564             * Ideally a Debian-based distribution (Ubuntu or Mint), but any other (e.g., CentOs, OpenSuse) should do as well.
565             * Perl 5 (>= 5.10 core; installed by default in most Linux distributions). Check the version with "perl -v"
566             * 1GB of RAM.
567             * 1 core (it only uses one core per job).
568             * At least 1GB HDD.
569              
570             =head1 HOW TO RUN BFF-PXF-SIMULATOR
571              
572             When run without any arguments, the software will use default settings. To modify any parameters, please refer to the synopsis for guidance.
573              
574             If you prefer not to include a specific term in the analysis, set its value to zero. For example:
575              
576             C<--treatments 0>
577              
578             B
579              
580             $ ./bff-pxf-simulator -f pxf # BFF with 100 samples
581              
582             $ ./bff-pxf-simulator -f pxf -n 1000 -o pxf.json # PXF with 1K samples and saved to pxf.json
583              
584             $ ./bff-pxf-simulator -phenotypicFeatures 10 # BFF with 100 samples and 10 pF each
585              
586             $ ./bff-pxf-simulator -diseases 0 -exposures 0 -procedures 0 -phenotypicFeatures 0 -treatments 0 # Only sex and ethnicity
587              
588             =head2 COMMON ERRORS AND SOLUTIONS
589              
590             * Error message: Foo
591             Solution: Bar
592              
593             * Error message: Foo
594             Solution: Bar
595              
596             =head1 AUTHOR
597              
598             Written by Manuel Rueda, PhD. Info about CNAG can be found at L.
599              
600             =head1 COPYRIGHT AND LICENSE
601              
602             This PERL file is copyrighted. See the LICENSE file included in this distribution.
603              
604             =cut