Physics-Ellipsometry-VASE

 view release on metacpan or  search on metacpan

lib/Physics/Ellipsometry/VASE.pm  view on Meta::CPAN

sub set_model {
    my ($self, $model) = @_;
    $self->{model} = $model;
}

# Fit data to model
sub fit {
    my ($self, $initial_params) = @_;
    my $data = $self->{data};
    my $model = $self->{model};

    # data shape is (nfields, npts); dim0=fields, dim1=data points
    # Extract x as (npts, 2) so $x->(:,0)=wavelength, $x->(:,1)=angle
    my $x_data = $data->(0:1,:)->xchg(0,1);

    # Build y to match model output order: [psi_all, delta_all]
    my $y_data = $data->((2),:)->flat->append($data->((3),:)->flat);

    # Use measured uncertainties from Woollam data when available
    my $sigma;
    if (defined $self->{sigma}) {
        $sigma = $self->{sigma}->((0),:)->flat->append($self->{sigma}->((1),:)->flat);
    } else {
        $sigma = ones($y_data->nelem);
    }

    # lmfit sizes $dyda from $x->getdim(0), so pass a dummy x whose
    # first dimension equals the number of y values (2*npts)
    my $x_fit = sequence($y_data->nelem);

    # Wrapper: adapts user model to lmfit interface ($x, $par, $ym, $dyda)
    my $fit_func = sub {
        my ($x, $par, $ym, $dyda) = @_;

        my $y_model = &$model($par, $x_data);
        $ym .= $y_model;

        # Numerical partial derivatives via finite differences
        my $np  = $par->nelem;
        for my $i (0 .. $np - 1) {
            my $par_h = $par->copy;
            my $p_i = $par->slice("($i)")->sclr;
            my $eps = abs($p_i) * 1e-7 + 1e-10;
            $par_h->slice("($i)") += $eps;
            $dyda->slice(",($i)") .= (&$model($par_h, $x_data) - $y_model) / $eps;
        }
    };

    my ($ym, $finalp, $covar, $iters) = lmfit(
        $x_fit, $y_data, $sigma, $fit_func, $initial_params,
        {Maxiter => 300, Eps => 1e-7}
    );

    $self->{covar} = $covar;
    $self->{iters} = $iters;
    $self->{ym}    = $ym;

    return $finalp;
}

# Plot raw data with model fit overlay
sub plot {
    my ($self, $fit_params, %opts) = @_;
    require PDL::Graphics::Gnuplot;

    my $data  = $self->{data};
    my $model = $self->{model};

    my $wavelength = $data->((0),:)->flat;
    my $angles     = $data->((1),:)->flat;
    my $psi_data   = $data->((2),:)->flat;
    my $delta_data = $data->((3),:)->flat;

    # Evaluate model at fitted parameters
    my $x_data  = $data->(0:1,:)->xchg(0,1);
    my $y_model = &$model($fit_params, $x_data);
    my $npts    = $wavelength->nelem;
    my $psi_fit   = $y_model->slice("0:" . ($npts - 1));
    my $delta_fit = $y_model->slice("$npts:" . (2 * $npts - 1));

    my $output = $opts{output};
    my $title  = $opts{title} // 'VASE Fit Results';

    # Find unique angles for grouping
    my @unique_angles = sort { $a <=> $b }
                        do { my %s; grep { !$s{$_}++ } list $angles };

    # Color palette for multiple angles
    my @colors = ('#0072B2', '#D55E00', '#009E73', '#CC79A7', '#F0E442',
                  '#56B4E9', '#E69F00', '#000000');

    # Select terminal and construct gpwin
    my $gp;
    if ($output) {
        my ($term, @topts);
        if    ($output =~ /\.png$/i) { $term = "pngcairo"; @topts = (size => [900,700,"px"]) }
        elsif ($output =~ /\.pdf$/i) { $term = "pdfcairo"; @topts = (size => [7,5.5,"in"]) }
        elsif ($output =~ /\.svg$/i) { $term = "svg";      @topts = (size => [900,700,"px"]) }
        elsif ($output =~ /\.eps$/i) { $term = "epscairo" }
        else                         { $term = "pngcairo"; @topts = (size => [900,700,"px"]) }
        $gp = PDL::Graphics::Gnuplot::gpwin($term, output => $output, enhanced => 1, @topts);
    } else {
        $gp = PDL::Graphics::Gnuplot::gpwin(enhanced => 1);
    }

    # Multiplot: Psi on top, Delta on bottom
    $gp->multiplot(layout => [1, 2], title => $title);

    # Build plot curves grouped by angle
    my (@psi_curves, @delta_curves);
    for my $ai (0 .. $#unique_angles) {
        my $ang = $unique_angles[$ai];
        my $mask = ($angles == $ang);
        my $idx = which($mask);
        my $wl   = $wavelength->index($idx);
        my $psid = $psi_data->index($idx);
        my $deld = $delta_data->index($idx);
        my $psif = $psi_fit->index($idx);
        my $delf = $delta_fit->index($idx);
        my $col  = $colors[$ai % scalar @colors];
        my $label = sprintf("%.1f{/Symbol \260}", $ang);

lib/Physics/Ellipsometry/VASE.pm  view on Meta::CPAN

=head1 SYNOPSIS

    use PDL;
    use PDL::NiceSlice;
    use Physics::Ellipsometry::VASE;

    # Create a VASE fitter for a single-layer model
    my $vase = Physics::Ellipsometry::VASE->new(layers => 1);

    # Load experimental data (auto-detects Woollam format)
    $vase->load_data('measurement.dat');

    # Define an optical model
    sub cauchy_model {
        my ($params, $x) = @_;
        my $wavelength = $x->(:,0);   # nm
        my $psi   = $params->(0) + $params->(1) / $wavelength**2;
        my $delta = $params->(2) + $params->(3) * $wavelength;
        return cat($psi, $delta)->flat;
    }

    $vase->set_model(\&cauchy_model);

    my $fitted = $vase->fit(pdl [45, 1e4, 120, 0.01]);

    # Plot results (requires PDL::Graphics::Gnuplot)
    $vase->plot($fitted, output => 'fit.png');

=head1 DESCRIPTION

Physics::Ellipsometry::VASE provides a framework for fitting optical thin-film
models to variable angle spectroscopic ellipsometry (VASE) data using the
Levenberg-Marquardt algorithm.

Ellipsometry measures the change in polarization state of light reflected from
a sample surface.  The two measured quantities are B<Psi> (related to the
amplitude ratio of p- and s-polarized reflectances) and B<Delta> (the phase
difference).  By fitting a physical model to these measurements across
wavelength and angle of incidence, one can extract optical constants (refractive
index, extinction coefficient) and film thicknesses.

The module handles:

=over 4

=item *

Loading data in both simple whitespace-delimited format and the native
J.A. Woollam VASE instrument format (auto-detected).

=item *

Automatic numerical Jacobian computation via relative-step finite differences.

=item *

Weighted fitting using measured uncertainties (sigma columns in Woollam files).

=item *

Multi-angle plotting of data and fit overlays via L<PDL::Graphics::Gnuplot>.

=back

=head1 CONSTRUCTOR

=head2 new

    my $vase = Physics::Ellipsometry::VASE->new(%args);

Creates a new VASE analysis object.

=over 4

=item B<layers> (optional, default 1)

Number of thin-film layers in the optical model.  Currently informational;
the actual layer structure is encoded in the user-supplied model function.

=item B<model> (optional)

A code reference for the model function.  Can also be set later with
L</set_model>.

=back

=head1 METHODS

=head2 load_data

    my $data = $vase->load_data($filename);

Reads ellipsometry data from C<$filename>.  The format is auto-detected:

=over 4

=item B<Simple format>

Whitespace-separated columns.  Lines starting with C<#> are comments; blank
lines are skipped.

    # Wavelength(nm)  Angle(deg)  Psi(deg)  Delta(deg)
    400  70  45.0  120.0
    410  70  44.5  121.0

=item B<Woollam VASE format>

Recognised when line 2 starts with C<VASEmethod[>.  The four-line header is
parsed and stored as object attributes:

    $vase->{sample_name}    # line 1
    $vase->{vase_method}    # VASEmethod[...] content
    $vase->{original_file}  # Original[...] content
    $vase->{units}          # line 4 (e.g. "nm")

Columns 5-6 (sigma_psi, sigma_delta) are extracted into C<< $vase->{sigma} >>
and automatically used as weights during fitting.

=back

Returns a PDL piddle of shape C<(4, npts)> where the columns are wavelength,

lib/Physics/Ellipsometry/VASE.pm  view on Meta::CPAN

        # $params: PDL piddle of fit parameters
        # $x:      PDL piddle of shape (npoints, 2)
        #          $x->(:,0) = wavelength (nm)
        #          $x->(:,1) = angle of incidence (deg)

        my $psi   = ...;   # compute Psi  (npoints)
        my $delta = ...;   # compute Delta (npoints)

        return cat($psi, $delta)->flat;
    }

The Jacobian (partial derivatives with respect to parameters) is
computed automatically via numerical finite differences.

=head1 METHODS

=head2 new

    my $vase = Physics::Ellipsometry::VASE->new(%args);

Constructor.  Accepts:

=over 4

=item layers

Number of layers in the optical model (default: 1).

=item model

Optional code reference to a model function.

=back

=head2 load_data

    $vase->load_data($filename);

Reads ellipsometry data from a whitespace-delimited file.
Returns the loaded data as a PDL piddle.

=head2 set_model

    $vase->set_model(\&model_func);

Sets the model function used for fitting.

=head2 fit

    my $fitted_params = $vase->fit($initial_params);

Performs Levenberg-Marquardt fitting.  C<$initial_params> is a PDL
piddle of initial guesses.  Returns a PDL piddle of fitted parameters.

=head2 plot

    $vase->plot($fit_params);
    $vase->plot($fit_params, output => 'fit.png');
    $vase->plot($fit_params, output => 'fit.pdf', title => 'My Fit');

Plots raw data points with model fit overlay in a two-panel layout
(Psi on top, Delta on bottom).  Requires L<PDL::Graphics::Gnuplot>.

Options:

=over 4

=item output

File path for saving the plot.  Format is inferred from the extension
(C<.png>, C<.pdf>, C<.svg>, C<.eps>).  If omitted, displays an
interactive window.

=item title

Overall plot title (default: C<VASE Fit Results>).

=back

=head1 DEPENDENCIES

L<PDL>, L<PDL::Fit::LM>, L<PDL::NiceSlice>

L<PDL::Graphics::Gnuplot> is required only for the C<plot> method.

=head1 AUTHOR

jtrujil1

=head1 LICENSE AND COPYRIGHT

This software is copyright (c) 2026.

This is free software; you can redistribute it and/or modify it under
the same terms as the Perl 5 programming language system itself.

=cut



( run in 1.014 second using v1.01-cache-2.11-cpan-39bf76dae61 )