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