The Perl and Raku Conference 2025: Greenville, South Carolina - June 27-29 Learn more

package NBI::Opts;
#ABSTRACT: A class for representing a the SLURM options for NBI::Slurm
use 5.012;
use Carp qw(confess);
$Data::Dumper::Sortkeys = 1;
$NBI::Opts::VERSION = $NBI::Slurm::VERSION;
my $SYSTEM_TEMPDIR = $ENV{'TMPDIR'} || $ENV{'TEMP'} || "/tmp";
require Exporter;
our @ISA = qw(Exporter);
sub _yell {
my $msg = shift @_;
my $col = shift @_ || "bold green";
say STDERR color($col), "[NBI::Opts]", color("reset"), " $msg";
}
sub new {
my $class = shift @_;
my ($queue, $memory, $threads, $opts_array, $tmpdir, $hours, $email_address, $email_when, $files, $placeholder) = (undef, undef, undef, undef, undef, undef, undef, undef, undef);
# Descriptive instantiation with parameters -param => value
if (substr($_[0], 0, 1) eq '-') {
my %data = @_;
# Try parsing
for my $i (keys %data) {
# QUEUE
if ($i =~ /^-queue/) {
next unless (defined $data{$i});
$queue = $data{$i};
# THREADS
} elsif ($i =~ /^-threads/) {
next unless (defined $data{$i});
# Check it's an integer
if ($data{$i} =~ /^\d+$/) {
$threads = $data{$i};
} else {
confess "ERROR NBI::Seq: -threads expects an integer\n";
}
# MEMORY
} elsif ($i =~ /^-memory/) {
next unless (defined $data{$i});
$memory = _mem_parse_mb($data{$i});
# TMPDIR
} elsif ($i =~ /^-tmpdir/) {
next unless (defined $data{$i});
$tmpdir = $data{$i};
# MAIL ADDRESS
} elsif ($i =~ /^-(mail|email_address)/) {
next unless (defined $data{$i});
$email_address = $data{$i};
# WHEN MAIL
} elsif ($i =~ /^-(when|email_type)/) {
next unless (defined $data{$i});
$email_when = $data{$i};
# OPTS ARRAY
} elsif ($i =~ /^-opts/) {
next unless (defined $data{$i});
# in this case we expect an array
if (ref($data{$i}) ne "ARRAY") {
confess "ERROR NBI::Seq: -opts expects an array\n";
}
$opts_array = $data{$i};
# TIME
} elsif ($i =~ /^-time/) {
$hours = _time_to_hour($data{$i});
# PLACEHOLDER
} elsif ($i =~ /^-placeholder/) {
# check if placeholder contains special regex characters
if (not defined $data{$i}) {
confess "ERROR NBI::Seq: Placeholder cannot be empty\n";
}
if ($data{$i} =~ /[\*\+\?]/) {
confess "ERROR NBI::Seq: Placeholder cannot contain special regex characters\n";
}
$placeholder = $data{$i};
# ARRAY
} elsif ($i =~ /^-files/) {
# expects ref to array
if (ref($data{$i}) ne "ARRAY") {
confess "ERROR NBI::Seq: -files expects an array\n";
} else {
$files = $data{$i};
}
} else {
confess "ERROR NBI::Seq: Unknown parameter $i\n";
}
}
}
my $self = bless {}, $class;
# Set attributes
$self->queue = defined $queue ? $queue : "nbi-short";
$self->threads = defined $threads ? $threads : 1;
$self->memory = defined $memory ? $memory : 100;
$self->hours = defined $hours ? $hours : 1;
$self->tmpdir = defined $tmpdir ? $tmpdir : $SYSTEM_TEMPDIR;
$self->email_address = defined $email_address ? $email_address : undef;
$self->email_type = defined $email_when ? $email_when : "none";
$self->files = defined $files ? $files : [];
$self->placeholder = defined $placeholder ? $placeholder : "#FILE#";
# Set options
$self->opts = defined $$opts_array[0] ? $opts_array : [];
return $self;
}
sub queue : lvalue {
# Update queue
my ($self, $new_val) = @_;
$self->{queue} = $new_val if (defined $new_val);
return $self->{queue};
}
sub threads : lvalue {
# Update threads
my ($self, $new_val) = @_;
$self->{threads} = $new_val if (defined $new_val);
return $self->{threads};
}
sub memory : lvalue {
# Update memory
my ($self, $new_val) = @_;
$self->{memory} = _mem_parse_mb($new_val) if (defined $new_val);
return $self->{memory};
}
sub email_address : lvalue {
# Update memory
my ($self, $new_val) = @_;
$self->{email_address} = $new_val if (defined $new_val);
return $self->{email_address};
}
sub email_type : lvalue {
# Update memory
my ($self, $new_val) = @_;
$self->{email_type} = $new_val if (defined $new_val);
return $self->{email_type};
}
# property files (list of files)
sub files : lvalue {
# Update files
my ($self, $new_val) = @_;
$self->{files} = $new_val if (defined $new_val);
return $self->{files};
}
sub placeholder : lvalue {
# Update placeholder
my ($self, $new_val) = @_;
$self->{placeholder} = $new_val if (defined $new_val);
return $self->{placeholder};
}
sub is_array {
# Check if the job is an array
my $self = shift @_;
return scalar @{$self->{files}} > 0;
}
sub hours : lvalue {
# Update memory
my ($self, $new_val) = @_;
$self->{hours} = _time_to_hour($new_val) if (defined $new_val);
return $self->{hours};
}
sub tmpdir : lvalue {
# Update tmpdir
my ($self, $new_val) = @_;
$self->{tmpdir} = $new_val if (defined $new_val);
return $self->{tmpdir};
}
sub opts : lvalue {
# Update opts
my ($self, $new_val) = @_;
if (not defined $self->{opts}) {
$self->{opts} = [];
return $self->{opts};
}
# check newval is an array
confess "ERROR NBI::Opts: opts must be an array, got $new_val\n" if (ref($new_val) ne "ARRAY");
$self->{opts} = $new_val if (defined $new_val);
return $self->{opts};
}
sub add_option {
# Add an option
my ($self, $new_val) = @_;
push @{$self->{opts}}, $new_val;
return $self->{opts};
}
sub opts_count {
# Return the number of options
my $self = shift @_;
return defined $self->{opts} ? scalar @{$self->{opts}} : 0;
}
sub view {
# Return a string representation of the object
my $self = shift @_;
my $str = " --- NBI::Opts object ---\n";
$str .= " queue:\t" . $self->{queue} . "\n";
$str .= " threads:\t" . $self->{threads} . "\n";
$str .= " memory MB:\t" . $self->{memory} . "\n";
$str .= " time (h):\t" . $self->{hours} . "\n";
$str .= " tmpdir:\t" . $self->{tmpdir} . "\n";
$str .= " ---------------------------\n";
for my $o (@{$self->{opts}}) {
$str .= "#SBATCH $o\n" if defined $o;
}
return $str;
}
sub header {
# Return a header for the script based on the options
my $self = shift @_;
my $str = "#!/bin/bash\n";
# Queue
$str .= "#SBATCH -p " . $self->{queue} . "\n";
# Nodes: 1
$str .= "#SBATCH -N 1\n";
# Time
$str .= "#SBATCH -t " . $self->timestring() . "\n";
# Memory
$str .= "#SBATCH --mem=" . $self->{memory} . "\n";
# Threads
$str .= "#SBATCH -c " . $self->{threads} . "\n";
# Mail
if (defined $self->{email_address}) {
$str .= "#SBATCH --mail-user=" . $self->{email_address} . "\n";
$str .= "#SBATCH --mail-type=" . $self->{email_type} . "\n";
}
# Custom options
for my $o (@{$self->{opts}}) {
next if not defined $o;
$str .= "#SBATCH $o\n";
}
# Job array
if ($self->is_array()) {
my $len = scalar @{$self->{files}} - 1;
$str .= "#SBATCH --array=0-$len\n";
}
return $str;
}
sub timestring {
my $self = shift @_;
my $hours = $self->{hours};
my $days = 0+ int($hours / 24);
$hours = $hours % 24;
# Format hours to be 2 digits
$hours = sprintf("%02d", $hours);
return "${days}-${hours}:00:00";
}
sub _mem_parse_mb {
my $mem = shift @_;
if ($mem=~/^(\d+)$/) {
# bare number: interpret as MB
return $mem;
} elsif ($mem=~/^(\d+)\.?(MB?|GB?|TB?|KB?)$/i) {
if (substr(uc($2), 0, 1) eq "G") {
$mem = $1 * 1024;
} elsif (substr(uc($2), 0, 1) eq "T") {
$mem = $1 * 1024 * 1024;
} elsif (substr(uc($2), 0, 1) eq "M") {
$mem = $1;
} elsif (substr(uc($2), 0, 1) eq "K") {
$mem = int($1/1024);
} else {
# Consider MB
$mem = $1;
}
} else {
confess "ERROR NBI::Opts: Cannot parse memory value $mem\n";
}
return $mem;
}
sub _time_to_hour {
# Get an integer (hours) or a string in the format \d+D \d+H \d+M \d+S
my $time = shift @_;
$time = uc($time);
if ($time =~ /^(\d+)$/) {
# Got an integer
return $1;
} else {
my $hours = 0;
while ($time =~ /(\d+)([DHMS])/g) {
my $val = $1;
my $unit = $2;
if ($unit eq "D") {
$hours += $val * 24;
} elsif ($unit eq "H") {
$hours += $val;
} elsif ($unit eq "M") {
$hours += $val / 60;
} elsif ($unit eq "S") {
$hours += $val / 3600;
} else {
die "ERROR NBI::Opts: Cannot parse time value $time\n";
}
}
return $hours;
}
}
1;
__END__
=pod
=encoding UTF-8
=head1 NAME
NBI::Opts - A class for representing a the SLURM options for NBI::Slurm
=head1 VERSION
version 0.12.0
=head1 SYNOPSIS
SLURM Options for L<NBI::Slurm>, to be passed to a L<NBI::Job> object.
use NBI::Opts;
my $opts = NBI::Opts->new(
-queue => "short",
-threads => 4,
-memory => "8GB",
-time => "2h",
-opts => [],
);
=head1 DESCRIPTION
The C<NBI::Opts> module provides a class for representing the SLURM options used by L<NBI::Slurm> for job submission.
It allows you to set various options such as the queue, number of threads, allocated memory, execution time, input files, and more.
=head1 METHODS
=head2 new()
Create a new instance of C<NBI::Opts>. In this case this will imply using a job array over a list of files
my $opts = NBI::Opts->new(
-queue => "short",
-threads => 4,
-memory => "8GB",
-time => "2h",
-opts => ["--option=Value"],
-tmpdir => "/path/to/tmp",
-email_address => "user@example.com",
-email_type => "ALL",
-files => ["file1.txt", "file2.txt"],
-placeholder => "#FILE#"
);
This method creates a new C<NBI::Opts> object with the specified options. The following parameters are supported:
=over 4
=item * B<-queue> (string, optional)
The SLURM queue to submit the job to. Default is "nbi-short".
=item * B<-threads> (integer, optional)
The number of threads to allocate for the job. Default is 1.
=item * B<-memory> (string or integer, optional)
The allocated memory for the job. It can be specified as a bare number representing megabytes (e.g., 1024), or with a unit suffix (e.g., "8GB"). Default is 100 megabytes.
=item * B<-time> (string or integer, optional)
The time limit for the job. It can be specified as hours (e.g., 2) or as a string with time units (e.g., "2h", "1d 12h"). Default is 1 hour.
=item * B<-opts> (arrayref, optional)
An array reference containing additional SLURM options to be passed to the job script.
=item * B<-tmpdir> (string, optional)
The temporary directory to use for job execution. Default is the system's temporary directory.
=item * B<-email_address> (string, optional)
The email address for job notifications.
=item * B<-email_type> (string, optional)
The type of email notifications to receive (e.g., "NONE", "BEGIN", "END", "FAIL", "ALL"). Default is "NONE".
=item * B<-files> (arrayref, optional)
An array reference containing input files or file patterns for the job.
=item * B<-placeholder> (string, optional)
A placeholder string to be used in the command for input files. Default is "#FILE#".
=back
=head2 queue
Accessor method for the SLURM queue.
$opts->queue = "long";
my $queue = $opts->queue;
=head2 threads
Accessor method for the number of threads.
$opts->threads = 8;
my $threads = $opts->threads;
=head2 memory
Accessor method for the allocated memory.
$opts->memory = "16GB";
my $memory = $opts->memory;
=head2 email_address
Accessor method for the email address.
$opts->email_address = "user@example.com";
my $email_address = $opts->email_address;
=head2 email_type
Accessor method for the email notification type.
$opts->email_type = "ALL";
my $email_type = $opts->email_type;
=head2 hours
Accessor method for the execution time in hours.
$opts->hours = 24;
my $hours = $opts->hours;
=head2 tmpdir
Accessor method for the temporary directory.
$opts->tmpdir = "/path/to/tmpdir";
my $tmpdir = $opts->tmpdir;
=head2 opts
Accessor method for the additional SLURM options.
$opts->opts = ["--output=TestJob.out", "--mail-user user@example.com"];
my $opts_array = $opts->opts;
=head2 files
Accessor method for the input files or file patterns.
$opts->files = ["file1.txt", "*.fasta"];
my $files = $opts->files;
=head2 placeholder
Accessor method for the input file placeholder.
$opts->placeholder = "{INPUT}";
my $placeholder = $opts->placeholder;
=head2 add_option
Add an additional SLURM option.
$opts->add_option("--output=TestJob.out");
=head2 opts_count
Get the number of additional SLURM options.
my $count = $opts->opts_count;
=head2 is_array
Check if the job is an array job (has multiple input files).
my $is_array = $opts->is_array;
=head2 view
Get a string representation of the options.
my $str = $opts->view;
=head2 header
Generate the SLURM header for the job script.
my $header = $opts->header;
=head2 timestring
Get the execution time as a formatted string.
my $timestring = $opts->timestring;
Returns the execution time in the format "DD-HH:MM:SS".
=head1 INTERNAL METHODS
=head2 _mem_parse_mb
Parse memory input and convert to megabytes.
=head2 _time_to_hour
Convert time input to hours.
=cut
=head1 AUTHOR
Andrea Telatin <proch@cpan.org>
=head1 COPYRIGHT AND LICENSE
This software is Copyright (c) 2023-2025 by Andrea Telatin.
This is free software, licensed under:
The MIT (X11) License
=cut