#!/usr/bin/perl
######################################################################
## vulcan.pl: XML-RPC interface to Xray::Absorption providing web
##            services to various tables of x-ray absorption
##            coefficients
######################################################################

=for Copyright
 .
 Copyright (c) 2007-2008 Bruce Ravel (bravel AT bnl DOT gov).
 All rights reserved.
 .
 This file is free software; you can redistribute it and/or
 modify it under the same terms as Perl itself. See The Perl
 Artistic License.
 .
 This program is distributed in the hope that it will be useful,
 but WITHOUT ANY WARRANTY; without even the implied warranty of
 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

=cut

use warnings;
use strict;
use version;
use Readonly;
use Getopt::Std;
Readonly my $VERSION => version->new("0.0.1");

##=======================================================================
## configuration section
##=======================

my %opt;
getopts('p:l:',\%opt);

# port on which to communicate
my $port = $opt{p} || '4444';

# system log file: set to an empty string to suppress logging
my $logfile = $opt{l} || '';

##=======================================================================

use Xray::Absorption;
Xray::Absorption -> load("Elam");
use Chemistry::Formula;

## CPAN modules -- These are additional pre-requisites
use Log::Handler;
use XMLRPC::Transport::HTTP;

use vars qw($log);
if ($logfile) {
  $log = Log::Handler->new(
			   filename => $logfile,
			   mode     => 'append',
			   newline  => 1,
			   maxlevel => 7,
			   minlevel => 0,
			  );
};

@::ISA = qw(XMLRPC::Server::Parameters);

my $daemon = XMLRPC::Transport::HTTP::Daemon
    -> new(LocalPort => $port, ReuseAddr => 1);
$daemon -> dispatch_to(qw(has available scattering
			  weight density conversion gamma
			  siegbahn iupac intensity
			  energy next_energy
			  xsec photo coherent incoherent f1 f2
			  fluor_yield edge_jump
			  formula
			  quit
			 ));
##$daemon -> on_action(sub{ $log->info(sprintf("Vulcan: %s", $daemon->http_request)) if $logfile });
$log->info("Vulcan $VERSION : XMLRPC server up at ", $daemon->url) if $logfile;
$daemon -> handle;


##------------------------------------------------------------
## utility to parse values from the hash reference that is the
## argument to almost all methods.  this also sets the data resource
sub _parse_hash {
  my ($rhash, $prefer_line) = @_;
  $prefer_line ||= 0;

  my $elem  = $$rhash{element}  || q{};
  my $edge  = q{};
  if ($prefer_line) {
    $edge = $$rhash{edge} || $$rhash{line} || q{};
  } else {
    $edge = $$rhash{line} || $$rhash{edge} || q{};
  };
  my $r_en  = $$rhash{energies} || [];
  my $r_el  = $$rhash{atoms}    || [];

  my $resource = $$rhash{resource} || "Elam";
  Xray::Absorption -> load($resource);

  return $elem, $edge, $r_en, $r_el;
};

sub quit {
  exit;
};

##------------------------------------------------------------
## database contents
sub available {
  my ($daemon) = @_;
  my @all = Xray::Absorption->available;
  return \@all;
};
sub scattering {
  my ($daemon) = @_;
  my @all = Xray::Absorption->scattering;
  return \@all;
};
sub has {
  my ($daemon, $rhash) = @_;
  my ($elem, $edge, $r_energies, $r_atoms) = _parse_hash($rhash,0);
  my $value = Xray::Absorption->in_resource($elem);
  return $value;
};

##------------------------------------------------------------
## atomic properties
sub weight {
  my ($daemon, $rhash) = @_;
  my ($elem, $edge, $r_energies, $r_atoms) = _parse_hash($rhash,0);
  my $value = Xray::Absorption->get_atomic_weight($elem);
  return $value;
};
sub density {
  my ($daemon, $rhash) = @_;
  my ($elem, $edge, $r_energies, $r_atoms) = _parse_hash($rhash,0);
  my $value = Xray::Absorption->get_density($elem);
  return $value;
};
sub conversion {
  my ($daemon, $rhash) = @_;
  my ($elem, $edge, $r_energies, $r_atoms) = _parse_hash($rhash,0);
  my $value = Xray::Absorption->get_conversion($elem);
  return $value;
};

##------------------------------------------------------------
## edge/line energies and symbols
sub energy {
  my ($daemon, $rhash) = @_;
  my ($elem, $edge, $r_energies, $r_atoms) = _parse_hash($rhash,0);
  my $value = Xray::Absorption->get_energy($elem, $edge);
  return $value;
  #return XMLRPC::Data->type(double => $value);
};
sub next_energy {
  my ($daemon, $rhash) = @_;
  my ($elem, $edge, $r_energies, $r_atoms) = _parse_hash($rhash,0);
  my @next = Xray::Absorption->next_energy($elem, $edge, @$r_atoms);
  return \@next;
  #return XMLRPC::Data->type(double => $value);
};
sub siegbahn {
  my ($daemon, $rhash) = @_;
  my ($elem, $line, $r_energies) = _parse_hash($rhash,1);
  my $value = Xray::Absorption->get_Siegbahn_full($line);
  return $value;
};
sub iupac {
  my ($daemon, $rhash) = @_;
  my ($elem, $line, $r_energies) = _parse_hash($rhash,1);
  my $value = Xray::Absorption->get_IUPAC($line);
  return $value;
};
sub intensity {
  my ($daemon, $rhash) = @_;
  my ($elem, $line, $r_energies) = _parse_hash($rhash,1);
  return 1 if (Xray::Absorption->current_resource !~ m{elam}i);
  my $value = Xray::Absorption->get_intensity($elem, $line);
  return sprintf("%g", $value);
};
sub gamma {
  my ($daemon, $rhash) = @_;
  my ($elem, $edge, $r_energies, $r_atoms) = _parse_hash($rhash,0);
  my $value = Xray::Absorption->get_gamma($elem, $edge);
  return $value;
};

sub fluor_yield {
  my ($daemon, $rhash) = @_;
  my ($elem, $edge, $r_energies, $r_atoms) = _parse_hash($rhash,0);
  my $value = Xray::Absorption->fluor_yield($elem, $edge);
  return $value;
};
sub edge_jump {
  my ($daemon, $rhash) = @_;
  my ($elem, $edge, $r_energies, $r_atoms) = _parse_hash($rhash,0);
  my $value = Xray::Absorption->edge_jump($elem, $edge);
  return $value;
};


##------------------------------------------------------------
## cross sections
sub xsec {
  pop;
  my ($daemon, $rhash) = @_;
  my ($elem, $edge, $r_energies, $r_atoms) = _parse_hash($rhash,0);
  my @values = Xray::Absorption->cross_section($elem, $r_energies);
  return \@values;
};
sub photo {
  pop;
  my ($daemon, $rhash) = @_;
  my ($elem, $edge, $r_energies, $r_atoms) = _parse_hash($rhash,0);
  my @values = Xray::Absorption->cross_section($elem, $r_energies, "photo");
  return \@values;
};
sub coherent {
  pop;
  my ($daemon, $rhash) = @_;
  my ($elem, $edge, $r_energies, $r_atoms) = _parse_hash($rhash,0);
  my $scattering = join("|", Xray::Absorption->scattering);
  return [] if (Xray::Absorption->current_resource =~ m{(?:$scattering)}i);
  my @values = Xray::Absorption->cross_section($elem, $r_energies, "coherent");
  return \@values;
};
sub incoherent {
  pop;
  my ($daemon, $rhash) = @_;
  my ($elem, $edge, $r_energies, $r_atoms) = _parse_hash($rhash,0);
  my $scattering = join("|", Xray::Absorption->scattering);
  return [] if (Xray::Absorption->current_resource =~ m{(?:$scattering)}i);
  my @values = Xray::Absorption->cross_section($elem, $r_energies, "incoherent");
  return \@values;
};
sub f1 {
  pop;
  my ($daemon, $rhash) = @_;
  my ($elem, $edge, $r_energies, $r_atoms) = _parse_hash($rhash,0);
  my $scattering = join("|", Xray::Absorption->scattering);
  return [] if (Xray::Absorption->current_resource !~ m{(?:$scattering)}i);
  my @values = Xray::Absorption->cross_section($elem, $r_energies, "f1");
  return \@values;
};
sub f2 {
  pop;
  my ($daemon, $rhash) = @_;
  my ($elem, $edge, $r_energies, $r_atoms) = _parse_hash($rhash,0);
  my $scattering = join("|", Xray::Absorption->scattering);
  return [] if (Xray::Absorption->current_resource !~ m{(?:$scattering)}i);
  my @values = Xray::Absorption->cross_section($elem, $r_energies, "f2");
  return \@values;
};

##------------------------------------------------------------
## Chemistry::Formula
sub formula {
  my ($daemon, $string) = @_;
  my %count;
  my $ok = Chemistry::Formula::parse_formula($string, \%count);
  return \%count if $ok;
  die "$count{error}\n";
};

exit;

=head1 NAME

Vulcan - XML-RPC server for data from Xray::Absorption

=head1 SYNOPSIS

This is a small and simple server for providing L<Xray::Absorption> as a
web service.  It uses XML-RPC to expose the methods of the
Xray::Absorption class to a client.

 Perl:
  use XMLRPC::Lite;
  my $client = XMLRPC::Lite -> proxy($HOST:$PORT);
  $rpcdata = $client -> in({element=>"Np"});
  ($rpcdata->result) ? print "Np is in this resource\n"
                     : print "Np is not in this resource\n" ;


 Python:
  To use this server from Python, it is recommended that you use the 
  VulcanClient module in ../client.  That client providess a more
  pythonic interface and ensures that a vulcan serveris running

  >>> import VulcanClient
  >>> vc = VulcanClient.VulcanClient()
  >>> vc.has('Np')
  True

  you can explicitly indicate which port to use:
  >>> vc = VulcanClient.VulcanClient(port=1221)

  otherwise one in the range 13600 - 13800 will be used.
  
  For a more raw interfalce, you can also use:
  >>> import xmlrpclib
  >>> server = xmlrpclib.Server(HOST:PORT)

  The python examples given below use 'vc' to indicate an instance
  of a VulcanClient.

=head1 DESCRIPTION

This server exposes the methods of L<Xray::Absorption> and
L<Chemistry::Elements> to clients over the web.  By poking at a vulcan
server, it would be easy to replicate the functionality of Hephaestus
as a web application.

The wonderful thing about L<XML-RPC|http://www.xmlrpc.com/> is that it
is language agnostic.  The example below are given in Perl and Python,
but implementations of XML-RPC exist for L<dozens of
languages|http://www.xmlrpc.com/directory/1568/implementations>.

=head1 METHODS

=head2 Method arguments

All methods take a hash reference as its argument, except for
C<available> and C<scattering> which take no arguments.  This hash
contains one or more of the following keys:

=over 4

=item I<element>

This can be a one- or two-letter element symbol, the element name, or
its Z number.

=item I<edge>

A string denoting an absorption edge, such as C<k> or C<M5>.

=item I<line>

A string denoting a fluorescence line.  This string can be Siegbahn
(C<Lbeta3>), shortened Siegbahn (C<Lb3>), or IUPAC (C<L1-M3>).

=item I<energies>

A reference to an array of energies.

=item I<atoms>

A reference to an array of atoms identified by symbol, name, or Z
number.  This is used by the C<next_energy> method.

=item I<resource>

The name of the data resource to use as the source for the data.

=back

Case does not matter for any of the string-valued hash entries.  Hash
entries not needed by a particular method are ignored.

The perl examples below use the syntax of an L<XMLRPC::Lite> client.

=head2 Database access

=over 4

=item C<available>

Return a list of all data resources known to Xray::Absorption.
Currently this list is

  Elam McMaster Henke Chantler CL Shaltout None

 Perl:
  $list = $client -> available;

 Python:
  >>> print vc.available
  ['Elam', 'McMaster', 'Henke', 'Chantler', 'CL']

=item C<scattering>

Return a list of all data resources known to Xray::Absorption which
include anomalous scattering values, f' and f".  Currently these are:

  Henke Chantler CL

 Perl:
  $density = $client -> scattering;

 Python:
  >>> print vc.scattering
  ['Henke', 'Chantler', 'CL']

=item C<has>

Returns true (1) if the element is in the current data resource and
false (0) if it is absent, Some of the data resources are incomplete
and each stops at a different actinide.

 Perl:
  $density = $client -> has({element => "Cu", resource => "McMaster"});

 Python:
  >>> print vc.has('Cu', resource='McMaster')
  True

=back

=head2 Atom properties

=over 4

=item C<energy>

Return the energy in eV of the specified edge or line.  Edges are
strings like C<k>, or C<M5>.  Lines can be Siegbahn (C<Lbeta3>),
shortened Siegbahn (C<Lb3>), or IUPAC (C<L1-M3>).

 Perl:
  $energy = $client -> energy({element => "Cu", edge => "K"});

 Python:
  >>> energy = vc.energy('Cu', edge='K')

=item C<next_energy>

Return the energy in eV of the next higest energy edge among a list of
atoms compared to the specified element and edge.  This returns a
reference to a three-element array containing the element, edge, and
edge energy of the next edge from the list of atoms.

 Perl:
  $energy = $client -> next_energy({element => "Ti",
                                    edge    => "K",
                                    atoms   => [qw(Ba Ti O)]
                                    });

 Python:
  >>> nextenergy = vc.next_energy('Cu', edge='K', atoms= ('Fe','Zn'))
  ('zn', 'k', 9659)

=item C<weight>

Return the atomic weight in atomic mass units of the specified.

 Perl:
  $weight = $client -> weight({element => "Cu"});

 Python:
  >>> weight = vc.weight('Cu')

=item C<density>

Return the density of the element in it's elemental, zero-valent,
standard form.

 Perl:
  $density = $client -> density({element => "Cu"});

 Python:
  >>> density = vc.density('Cu')

=item C<conversion>

Return the coeffient for converting cross-sections between barns and
grams per square centimeter.

 Perl:
  $conv = $client -> conversion({element => "Cu"});

 Python:
  >> conv = s.conversion('Cu')

=item C<siegbahn>

Return a line symbol in Siegbahn form.  The input can be Siegbahn
(C<Lbeta3>), shortened Siegbahn (C<Lb3>), or IUPAC (C<L1-M3>).

 Perl:
  $symbol = $client -> siegbahn({line => "Lb3"});

 Python:
  >>> symbol = vc.siegbahn(line='Lb3')

=item C<iupac>

Return a line symbol in IUPAC form.  The input can be Siegbahn
(C<Lbeta3>), shortened Siegbahn (C<Lb3>), or IUPAC (C<L1-M3>).

 Perl:
  $symbol = $client -> iupac({line => "Lb3"});

 Python:
  >>> symbol = vc.iupac(line='Lb3')

=item C<intensity>

Return the intensity of the given line.  The intensities of all lines
associated with a common core state sum to one.  For example, the
intensities of the 3 Kalpha and 5 Kbeta lines sum to one.

 Perl:
  $intensity = $client -> intensity({line => "Lb3"});

 Python:
  >>> intensity = vc.intensity(line ='Lb3')

=item C<gamma>

Return an approximation of the core-hole lifetime for an element and
edge.  This follows Feff very closely.  In fact the data used is
swiped from the setgam subroutine in Feff, which is in turn swiped
from K. Rahkonen and K. Krause, I<Atomic Data and Nuclear Data
Tables>, B<14>:2 (1974).  The values given by this routine are a bit
different from those given by Feff since a different interpolation is
used.  For O and P edges a value of 0.1 is returned, as in Feff.  If
the arguments are not interpretable, a value of 0 is returned.

 Perl:
  $intensity = $client -> gamma({element => "Cu", edge => "K"});

 Python:
  >>> gamma = vc.gamma('Cu',edge='K')

=item C<fluor_yield>

Return the fluorescence yield for an element and edge
Data comes from Krause, 1979

 Perl:
  $fy = $client -> fluor_yield({element => "Cu", edge => "K"});

 Python:
  >>> fy = vc.fluor_yield('Cu',edge='K')

=item C<edge_jump>

Return the edge jump ratio for an element and edge


 Perl:
  $jump = $client -> edge_jump({element => "Cu", edge => "K"});

 Python:
  >>> jump = vc.edge_jump('Cu',edge='K')


=back

=head2 Cross sections

All cross sections are in units of barns per atom.  Use the
C<conversion> to convert to grams per square centimeter.

=over 4

=item C<xsec>

Return a reference to a list of total cross sections at the specified
energies.  The total cross section is the sum of the photoelectric,
coherent, and incoherent cross sections.

 Perl:
  $xsec = $client -> xsec({element  => "Cu",
                           energies => [8700, 8800, 8900, 9000, 9100] });

 Python:
  >>> xsec = vc.xsec('Cu', energies=[8700, 8800, 8900, 9000, 9100])

=item C<photo>

Return a reference to a list of photoelectric cross sections at the
specified energies.

 Perl:
  $photo = $client -> photo({element  => "Cu",
                             energies => [8700, 8800, 8900, 9000, 9100] });

 Python:
  >>> photo = vc.photo('Cu', energies=[8700, 8800, 8900, 9000, 9100])

=item C<coherent>

Return a reference to a list of coherent cross sections at the
specified energies.  A reference to an empty list is returned when
using one of the data resources containing only anomalous scattering
factors.  See the C<scattering method>.

 Perl:
  $coh = $client -> coherent({element  => "Cu",
                              energies => [8700, 8800, 8900, 9000, 9100] });

 Python:
  >>> coh = vc.coherent('Cu', energies=[8700, 8800, 8900, 9000, 9100])

=item C<incoherent>

Return a reference to a list of incoherent cross sections at the
specified energies.  A reference to an empty list is returned when
using one of the data resources containing only anomalous scattering
factors.  See the C<scattering method>.

 Perl:
  $incoh = $client -> incoherent({element  => "Cu",
                                  energies => [8700, 8800, 8900, 9000, 9100] });

 Python:
  >>> incoh = vc.incoherent('Cu', energies=[8700, 8800, 8900, 9000, 9100])

=item C<f1>

Return a reference to a list of the real part of the anomalous
scattering factors at the specified energies for data resources which
inlcude anomalous scattering factors.  A reference to an empty list is
returned for other resources.

 Perl:
  $f1 = $client -> f1({element  => "Cu",
                       energies => [8700, 8800, 8900, 9000, 9100],
                       resource => "CL",
                      });

 Python:
  >>> f1 = vc.f1('Cu', energies=[8700, 8800, 8900, 9000, 9100], resource='CL')

=item C<f2>

Return a reference to a list of the imaginary part of the anomalous
scattering factors at the specified energies for data resources which
inlcude anomalous scattering factors.  A reference to an empty list is
returned for other resources.

 Perl:
  $f2 = $client -> f2({element  => "Cu",
                       energies => [8700, 8800, 8900, 9000, 9100],
                       resource => "CL",
                      });

 Python:
  f2 = s.f2({'element':'Cu',
             'energies':[8700, 8800, 8900, 9000, 9100]})

=back

=head2 Other methods

This server exposes other things beside the guts of Xray::Absorption.
Right now, the only other thing exposed is the main method of
L<Chemistry::Formula>.

=over 4

=item C<formula>

This method from L<Chemistry::Formula> attempts to parse a string as a
chemical formula.  It makes a few assumptions about the formatting of
the string.  (1) Element symbols must be first letter capitalized and
second letter (if there is one) lower case. (2) Numbers can be in any
form that a computer program typically recognizes as a number, but
exponential notation must lead with a number.  That is "1e-6" or
"1.0e-6" are acceptable, but "e-6" is not.  The leading letter may be
interpreted as part of an element symbol.  (3) Spaces are removed from
the string, so spaces can be inserted for legibility.  (4) Formulas in
the formats of TeX or Inspec are permissible.  So "PbTiO3",
"PbTiO$_3$", and "PbTiO/sub 3/" will all be interpreted identically.

This method return a reference to a hash containing the element counts
parsed from the string.  The hash keys are capitalized element symbols
and the hash values are the numbers of each element found in the
string.

Note that this method takes a B<string> as its argument, not a hash
reference.

 Perl:
  %count = %{ $client -> formula("PbTiO3") };
  while ((my ($k, $v)) = each %count) { printf "  %-2s  %d\n", $k, $v; };

 Python:


 this prints:
  O   3
  Ti  1
  Pb  1

Here is a more interesting example -- the contents of air.  This
demonstrates the use of exponential numbers, an element appearing in
multiple parts of the formula, and the use of parentheses.

 Perl:
  %count = %{ $client
      -> formula("(N2)0.78 (O2)0.21 (CO2)0.03 Ar0.01 Kr1e-6 Xe9e-7") };
  while ((my ($k, $v)) = each %count) { printf "  %-2s  %G\n", $k, $v; };

 Python:


 this prints:
  Xe  9E-07
  O   0.48
  Ar  0.01
  Kr  1E-06
  C   0.03
  N   1.56

=back

=head1 DIAGNOSTICS

The Xray::Absorption methods all return errors via the standard XMLRPC
channels.  Use $client->fault (or your language's equivalent) to
access the error message.

The C<formula> method returns its error in the return hash using
the hash key "error".  For instance

  %count = %{ $client->formula("Ni(CN4");
  print $count{error}, $/;
   --> 1 opening and 0 closing parentheses.

=head1 CONFIGURATION AND ENVIRONMENT

There are a few configuration variables near the top of the file,
including the port number used by the server and the name of the
run-time log file.

=head1 DEPENDENCIES

Along with L<Xray::Absorption> and L<Chemistry::Formula>, the
following non-core perl modules are used:

=over 4

=item *

L<version>

=item *

L<Readonly>

=item *

L<Log::Handler>

=item *

L<XMLRPC::Lite>

=back

=head1 BUGS AND LIMITATIONS

=over 4

=item *

Need to implement interface to the kalziumrc file used by Hephaestus.

=item *

Unimplemented: get_l, edge list, line list

=back

Please report problems to Bruce Ravel (bravel AT bnl DOT gov)

Patches are welcome.

=head1 AUTHOR

Bruce Ravel (bravel AT bnl DOT gov)

http://cars9.uchicago.edu/~ravel/software/

SVN repository: http://cars9.uchicago.edu/svn/libperlxray/


=head1 LICENCE AND COPYRIGHT

Copyright (c) 2007-2008 Bruce Ravel (bravel AT bnl DOT gov). All rights reserved.

This module is free software; you can redistribute it and/or
modify it under the same terms as Perl itself. See L<perlartistic>.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

=cut
