Transform_demo.pm (PDL-2.076) | : | Transform_demo.pm (PDL-2.077) | ||
---|---|---|---|---|

# | ||||

package PDL::Demos::Transform_demo; | package PDL::Demos::Transform_demo; | |||

use PDL; | ||||

use PDL::Graphics::PGPLOT::Window; | use PDL::Graphics::PGPLOT::Window; | |||

use PDL::Transform; | use PDL::Transform; | |||

require File::Spec; | ||||

use Carp; | ||||

use File::Spec; | sub info {('transform', 'Coordinate transformations (Req.: PGPLOT)')} | |||

PDL::Demos::Routines->import(); | ||||

sub comment($); | ||||

sub act($); | ||||

sub output; | ||||

sub run { | ||||

local($PDL::debug) = 0; | ||||

local($PDL::verbose) = 0; | ||||

sub init {' | ||||

##$ENV{PGPLOT_XW_WIDTH}=0.6; | ##$ENV{PGPLOT_XW_WIDTH}=0.6; | |||

$ENV{PGPLOT_DEV} = $^O =~ /MSWin32/ ? '/GW' : | $ENV{PGPLOT_DEV} = $^O =~ /MSWin32/ ? "/GW" : | |||

defined($ENV{PGPLOT_DEV}) ? $ENV{PGPLOT_DEV} : "/XWIN"; | defined($ENV{PGPLOT_DEV}) ? $ENV{PGPLOT_DEV} : "/XWIN"; | |||

use PDL::Graphics::PGPLOT::Window; | ||||

'} | ||||

# try and find m51.fits | # try and find m51.fits | |||

$d = File::Spec->catdir( "PDL", "Demos" ); | my @f = qw(PDL Demos m51.fits); | |||

$m51path = undef; | our $m51file = undef; | |||

foreach my $path ( @INC ) { | foreach my $path ( @INC ) { | |||

my $check = File::Spec->catdir( $path, $d ); | my $file = File::Spec->catfile( $path, @f ); | |||

if ( -d $check ) { $m51path = $check; last; } | if ( -f $file ) { $m51file = $file; last; } | |||

} | } | |||

barf "Unable to find directory ${m51path} within the perl libraries.\n" | confess "Unable to find m51.fits within the perl libraries.\n" | |||

unless defined $m51path; | unless defined $m51file; | |||

comment q| | my @demo = ( | |||

[comment => q| | ||||

This demo illustrates the PDL::Transform module. | This demo illustrates the PDL::Transform module. | |||

It requires PGPLOT support in PDL and makes use of the image of | It requires PGPLOT support in PDL and makes use of the image of | |||

M51 kindly provided by the Hubble Heritage group at the | M51 kindly provided by the Hubble Heritage group at the | |||

Space Telescope Science Institute. | Space Telescope Science Institute. | |||

|; | |], | |||

act q| | [act => q| | |||

# PDL::Transform objects embody coordinate transformations. | # PDL::Transform objects embody coordinate transformations. | |||

use PDL::Transform; | use PDL::Transform; | |||

# set up a simple linear scale-and-shift relation | # set up a simple linear scale-and-shift relation | |||

$t = t_linear( Scale=>[2,-1], Post=>[100,0]); | $t = t_linear( Scale=>[2,-1], Post=>[100,0]); | |||

print $t; | print $t; | |||

|; | |], | |||

act q| | [act => q| | |||

# The simplest way to use PDL::Transform is to transform a set of | # The simplest way to use PDL::Transform is to transform a set of | |||

# vectors. To do this you use the "apply" method. | # vectors. To do this you use the "apply" method. | |||

# Define a few 2-vectors: | # Define a few 2-vectors: | |||

$xy = pdl([[0,1],[1,2],[10,3]]); | $xy = pdl([[0,1],[1,2],[10,3]]); | |||

print "xy: ", $xy; | print "xy: ", $xy; | |||

# Transform the 2-vectors: | # Transform the 2-vectors: | |||

print "Transformed: ", $xy->apply( $t ); | print "Transformed: ", $xy->apply( $t ); | |||

|; | |], | |||

act q| | [act => q| | |||

# You can invert and compose transformations with 'x' and '!'. | # You can invert and compose transformations with 'x' and '!'. | |||

$u = t_linear( Scale=>10 ); # A new transformation (simple x10 scale) | $u = t_linear( Scale=>10 ); # A new transformation (simple x10 scale) | |||

$xy = pdl([[0,1],[10,3]]); # Two 2-vectors | $xy = pdl([[0,1],[10,3]]); # Two 2-vectors | |||

print "xy: ", $xy; | print "xy: ", $xy; | |||

print "xy': ", $xy->apply( !$t ); # Invert $t from earlier. | print "xy': ", $xy->apply( !$t ); # Invert $t from earlier. | |||

print "xy'': ", $xy->apply( $u x !$t ); # Hit the result with $u. | print "xy'': ", $xy->apply( $u x !$t ); # Hit the result with $u. | |||

|; | |], | |||

act q| | [act => q| | |||

# PDL::Transform is useful for data resampling, and that's perhaps | # PDL::Transform is useful for data resampling, and that's perhaps | |||

# the best way to demonstrate it. First, we do a little bit of prep work: | # the best way to demonstrate it. First, we do a little bit of prep work: | |||

# Read in an image ($m51path has been set up by this demo to | # Read in an image ($m51file has been set up by this demo to | |||

# contain the location of the file). Transform is designed to | # contain the location of the file). Transform is designed to | |||

# work well with FITS images that contain WCS scientific coordinate | # work well with FITS images that contain WCS scientific coordinate | |||

# information, but works equally well in pixel space. | # information, but works equally well in pixel space. | |||

$m51 = rfits("$m51path/m51.fits",{hdrcpy=>1}); | $m51 = rfits($|.__PACKAGE__.q|::m51file,{hdrcpy=>1}); | |||

# we use a floating-point version of the image in some of the demos | # we use a floating-point version of the image in some of the demos | |||

# to highlight the interpolation schemes. (Note that the FITS | # to highlight the interpolation schemes. (Note that the FITS | |||

# header gets deep-copied automatically into the new variable). | # header gets deep-copied automatically into the new variable). | |||

$m51_fl = $m51->float; | $m51_fl = $m51->float; | |||

# Define a nice, simple scale-by-3 transformation. | # Define a nice, simple scale-by-3 transformation. | |||

$ts = t_scale(3); | $ts = t_scale(3); | |||

|], | ||||

|; | [act => q| | |||

act q| | ||||

#### Resampling with ->map and no FITS interpretation works in pixel space. | #### Resampling with ->map and no FITS interpretation works in pixel space. | |||

### Create a PGPLOT window, and display the original image | ### Create a PGPLOT window, and display the original image | |||

$dev = $^O =~ /MSWin32/ ? '/GW' : | $dev = $^O =~ /MSWin32/ ? '/GW' : | |||

defined($ENV{PGPLOT_DEV}) ? $ENV{PGPLOT_DEV} : "/XW"; | defined($ENV{PGPLOT_DEV}) ? $ENV{PGPLOT_DEV} : "/XW"; | |||

$win = pgwin( dev=> $dev, nx=>2, ny=>2, Charsize=>2, J=>1, Size=>[8,6] ); | $win = pgwin( dev=> $dev, nx=>2, ny=>2, Charsize=>2, J=>1, Size=>[8,6] ); | |||

$win->imag( $m51 , { DrawWedge=>0, Title=>"M51" } ); | $win->imag( $m51 , { DrawWedge=>0, Title=>"M51" } ); | |||

### Grow m51 by a factor of 3; origin is at lower left | ### Grow m51 by a factor of 3; origin is at lower left | |||

skipping to change at line 119 | skipping to change at line 113 | |||

# space, ignoring the FITS header) | # space, ignoring the FITS header) | |||

$win->imag( $m51->map( $ts, {pix=>1} ) ); | $win->imag( $m51->map( $ts, {pix=>1} ) ); | |||

$win->label_axes("","","M51 grown by 3 (pixel coords)"); | $win->label_axes("","","M51 grown by 3 (pixel coords)"); | |||

### Shrink m51 by a factor of 3; origin still at lower left. | ### Shrink m51 by a factor of 3; origin still at lower left. | |||

# (You can invert the transform with a leading '!'.) | # (You can invert the transform with a leading '!'.) | |||

$win->imag( $m51->map( !$ts, {pix=>1} ) ); | $win->imag( $m51->map( !$ts, {pix=>1} ) ); | |||

$win->label_axes("","","M51 shrunk by 3 (pixel coords)"); | $win->label_axes("","","M51 shrunk by 3 (pixel coords)"); | |||

|], | ||||

|; | [act => q| | |||

act q| | ||||

# You can work in scientific space (or any other space) by | # You can work in scientific space (or any other space) by | |||

# wrapping your main transformation with something that translates | # wrapping your main transformation with something that translates | |||

# between the coordinates you want to act in, and the coordinates | # between the coordinates you want to act in, and the coordinates | |||

# you have. Here, "t_fits" translates between pixels in the data | # you have. Here, "t_fits" translates between pixels in the data | |||

# and arcminutes in the image plane. | # and arcminutes in the image plane. | |||

### Clear the panel and start over | ### Clear the panel and start over | |||

$win->panel(4); # (Clear whole window on next plot) | $win->panel(4); # (Clear whole window on next plot) | |||

$win->imag( $m51, { Title=>"M51" } ); | $win->imag( $m51, { Title=>"M51" } ); | |||

### Scale in scientific coordinates. | ### Scale in scientific coordinates. | |||

# Here's a way to scale in scientific coordinates: | # Here's a way to scale in scientific coordinates: | |||

# wrap our transformation in FITS-header transforms to translate | # wrap our transformation in FITS-header transforms to translate | |||

# the transformation into scientific space. | # the transformation into scientific space. | |||

$win->imag( $m51->map( !$ts->wrap(t_fits($m51)), {pix=>1} ) ); | $win->imag( $m51->map( !$ts->wrap(t_fits($m51)), {pix=>1} ) ); | |||

$win->label_axes("","","M51 shrunk 3x (sci. coords)"); | $win->label_axes("","","M51 shrunk 3x (sci. coords)"); | |||

|], | ||||

|; | [act => q| | |||

act q| | ||||

# If you don't specify "pix=>1" then the resampler works in scientific | # If you don't specify "pix=>1" then the resampler works in scientific | |||

# FITS coordinates (if the image has a FITS header): | # FITS coordinates (if the image has a FITS header): | |||

### Scale in scientific coordinates (origin at center of galaxy) | ### Scale in scientific coordinates (origin at center of galaxy) | |||

$win->fits_imag( $m51->map( $ts, $m51->hdr ), { Title=>"M51 3x" } ); | $win->fits_imag( $m51->map( $ts, $m51->hdr ), { Title=>"M51 3x" } ); | |||

### Instead of setting up a coordinate transformation you can use the | ### Instead of setting up a coordinate transformation you can use the | |||

# implicit FITS header matching. Just tweak the template header: | # implicit FITS header matching. Just tweak the template header: | |||

$tohdr = $m51->hdr_copy; | $tohdr = $m51->hdr_copy; | |||

$tohdr->{CDELT1} /= 3; # Magnify 3x in horiz direction | $tohdr->{CDELT1} /= 3; # Magnify 3x in horiz direction | |||

$tohdr->{CDELT2} /= 3; # Magnify 3x in vert direction | $tohdr->{CDELT2} /= 3; # Magnify 3x in vert direction | |||

### Resample to match the new FITS header | ### Resample to match the new FITS header | |||

# (Note that, although the image is scaled exactly the same as before, | # (Note that, although the image is scaled exactly the same as before, | |||

# this time the scientific coordinates have scaled too.) | # this time the scientific coordinates have scaled too.) | |||

$win->fits_imag( $m51->map( t_identity(), $tohdr ), { Title=>"3x (FITS)" } ); | $win->fits_imag( $m51->map( t_identity(), $tohdr ), { Title=>"3x (FITS)" } ); | |||

|; | |], | |||

act q| | [act => q| | |||

### The three main resampling methods are "sample", "linear", and "jacobian". | ### The three main resampling methods are "sample", "linear", and "jacobian". | |||

# Sampling is fastest, linear interpolation is better. Jacobian resampling | # Sampling is fastest, linear interpolation is better. Jacobian resampling | |||

# is slow but prevents aliasing under skew or reducing transformations. | # is slow but prevents aliasing under skew or reducing transformations. | |||

$win->fits_imag( $m51_fl , {Title=>"M51"} ); | $win->fits_imag( $m51_fl , {Title=>"M51"} ); | |||

$win->fits_imag( $m51_fl->map( $ts, $m51_fl, { method=>"sample" } ), | $win->fits_imag( $m51_fl->map( $ts, $m51_fl, { method=>"sample" } ), | |||

{Title=>"M51 x3 (sampled)"} ); | {Title=>"M51 x3 (sampled)"} ); | |||

$win->fits_imag( $m51_fl->map( $ts, $m51_fl, { method=>"linear" } ), | $win->fits_imag( $m51_fl->map( $ts, $m51_fl, { method=>"linear" } ), | |||

{ Title=>"M51 x3 (interp.)"} ); | { Title=>"M51 x3 (interp.)"} ); | |||

$win->fits_imag( $m51_fl->map( $ts, $m51_fl, { method=>"jacobian" } ), | $win->fits_imag( $m51_fl->map( $ts, $m51_fl, { method=>"jacobian" } ), | |||

{ Title=>"M51 x3 (jacob.)"} ); | { Title=>"M51 x3 (jacob.)"} ); | |||

|], | ||||

|; | [act => q| | |||

act q| | ||||

### Linear transformations are only the beginning. Here's an example | ### Linear transformations are only the beginning. Here's an example | |||

# using a simple nonlinear transformation: radial coordinate transformation. | # using a simple nonlinear transformation: radial coordinate transformation. | |||

### Original image | ### Original image | |||

$win->fits_imag( $m51 ,{Title=>"M51"}); | $win->fits_imag( $m51 ,{Title=>"M51"}); | |||

### Radial structure in M51 (linear radial scale; origin at (0,0) by default) | ### Radial structure in M51 (linear radial scale; origin at (0,0) by default) | |||

$tu = t_radial( u=>'degree' ); | $tu = t_radial( u=>'degree' ); | |||

$win->fits_imag( $m51_fl->map($tu), { Title=>"M51 radial (linear)", J=>0}); | $win->fits_imag( $m51_fl->map($tu), { Title=>"M51 radial (linear)", J=>0}); | |||

### Radial structure in M51 (conformal/logarithmic radial scale) | ### Radial structure in M51 (conformal/logarithmic radial scale) | |||

$tu_c = t_radial( r0=>0.1 ); # Y axis 0 is at 0.1 arcmin | $tu_c = t_radial( r0=>0.1 ); # Y axis 0 is at 0.1 arcmin | |||

$win->panel(3); | $win->panel(3); | |||

$win->fits_imag( $m51_fl->map($tu_c), | $win->fits_imag( $m51_fl->map($tu_c), | |||

{ Title=>"M51 radial (conformal)", | { Title=>"M51 radial (conformal)", | |||

YRange=>[0,4] } ); | YRange=>[0,4] } ); | |||

|], | ||||

|; | ||||

# NOTE: | # NOTE: | |||

# need to 'double protect' the \ in the label_axes() | # need to 'double protect' the \ in the label_axes() | |||

# since it's being evaluated twice (I think) | # since it's being evaluated twice (I think) | |||

# | # | |||

act q| | [act => q| | |||

##################### | ##################### | |||

# Wrapping transformations allows you to work in a convenient | # Wrapping transformations allows you to work in a convenient | |||

# space for what you want to do. Here, we can use a simple | # space for what you want to do. Here, we can use a simple | |||

# skew matrix to find (and remove) logarithmic spiral structures in | # skew matrix to find (and remove) logarithmic spiral structures in | |||

# the galaxy. The "unspiraled" images shift the spiral arms into | # the galaxy. The "unspiraled" images shift the spiral arms into | |||

# approximate straight lines. | # approximate straight lines. | |||

$sp = 3.14159; # Skew by 3.14159 | $sp = 3.14159; # Skew by 3.14159 | |||

# Skew matrix | # Skew matrix | |||

$t_skew = t_linear(pre => [$sp * 130, 0] , matrix => pdl([1,0],[-$sp,1])); | $t_skew = t_linear(pre => [$sp * 130, 0] , matrix => pdl([1,0],[-$sp,1])); | |||

# When put into conformal radial space, the skew turns into 3.14159 | # When put into conformal radial space, the skew turns into 3.14159 | |||

# radians per scale height. | # radians per scale height. | |||

$t_untwist = t_wrap($t_skew, $tu_c); | $t_untwist = t_wrap($t_skew, $tu_c); | |||

# Press enter to see the result of these transforms... | # Press enter to see the result of these transforms... | |||

|; | |], | |||

act q| | [act => q| | |||

############################## | ############################## | |||

# Note that you can use ->map and ->unmap as either PDL methods | # Note that you can use ->map and ->unmap as either PDL methods | |||

# or transform methods; what to do is clear from context. | # or transform methods; what to do is clear from context. | |||

# Original image | # Original image | |||

$win->fits_imag($m51, {Title => "M51"} ); | $win->fits_imag($m51, {Title => "M51"} ); | |||

# Skewed | # Skewed | |||

$win->fits_imag( $m51_fl->map( $t_skew ), | $win->fits_imag( $m51_fl->map( $t_skew ), | |||

{ Title => "M51 skewed by \\\\gp in spatial coords" } ); | { Title => "M51 skewed by \\\\gp in spatial coords" } ); | |||

# Untwisted -- show that m51 has a half-twist per scale height | # Untwisted -- show that m51 has a half-twist per scale height | |||

$win->fits_imag( $m51_fl->map( $t_untwist ), | $win->fits_imag( $m51_fl->map( $t_untwist ), | |||

{ Title => "M51 unspiraled (\\\\gp / r\\\\ds\\\\u)"} ); | { Title => "M51 unspiraled (\\\\gp / r\\\\ds\\\\u)"} ); | |||

# Untwisted -- the jacobean method uses variable spatial filtering | # Untwisted -- the jacobean method uses variable spatial filtering | |||

# to eliminate spatial artifacts, at significant computational cost | # to eliminate spatial artifacts, at significant computational cost | |||

# (This may take some time to complete). | # (This may take some time to complete). | |||

$win->fits_imag( $m51_fl->map( $t_untwist, {m=>jacobean}), | $win->fits_imag( $m51_fl->map( $t_untwist, {m=>jacobean}), | |||

{ Title => "M51 unspiraled (\\\\gp / r\\\\ds\\\\u; antialiased)" } ); | { Title => "M51 unspiraled (\\\\gp / r\\\\ds\\\\u; antialiased)" } ); | |||

|; | |], | |||

$win->close; | ||||

act q| | [act => q| | |||

$win->close; | ||||

### Native FITS interpretation makes it easy to view your data in | ### Native FITS interpretation makes it easy to view your data in | |||

### your preferred coordinate system. Here we zoom in on a 0.2x0.2 | ### your preferred coordinate system. Here we zoom in on a 0.2x0.2 | |||

### arcmin region of M51, sampling it to 100x100 pixels resolution. | ### arcmin region of M51, sampling it to 100x100 pixels resolution. | |||

$m51 = float $m51; | $m51 = float $m51; | |||

$data = $m51->match([100,100],{or=>[[-0.05,0.15],[-0.05,0.15]]}); | $data = $m51->match([100,100],{or=>[[-0.05,0.15],[-0.05,0.15]]}); | |||

$s = "M51 closeup ("; $ss=" coords)"; | $s = "M51 closeup ("; $ss=" coords)"; | |||

$ps = " (pixels)"; | $ps = " (pixels)"; | |||

$dev = $^O =~ /MSWin32/ ? '/GW' : | $dev = $^O =~ /MSWin32/ ? '/GW' : | |||

skipping to change at line 274 | skipping to change at line 263 | |||

$w1->imag( $data, 600, 750, { title=>"${s}pixel${ss}", | $w1->imag( $data, 600, 750, { title=>"${s}pixel${ss}", | |||

xtitle=>"X$ps", ytitle=>"Y$ps" } ); | xtitle=>"X$ps", ytitle=>"Y$ps" } ); | |||

$w1->hold; | $w1->hold; | |||

$w2 = pgwin( dev=> $dev, size=>[4,4], charsize=>1.5, justify=>1 ); | $w2 = pgwin( dev=> $dev, size=>[4,4], charsize=>1.5, justify=>1 ); | |||

$w2->fits_imag( $data, 600, 750, { title=>"${s}sci.${ss}", dr=>0 } ); | $w2->fits_imag( $data, 600, 750, { title=>"${s}sci.${ss}", dr=>0 } ); | |||

$w2->hold; | $w2->hold; | |||

# Now please separate the two X windows on your screen, and press ENTER. | # Now please separate the two X windows on your screen, and press ENTER. | |||

############################### | ############################### | |||

|; | |], | |||

act q| | [act => q| | |||

### Now rotate the image 360 degrees in 10 degree increments. | ### Now rotate the image 360 degrees in 10 degree increments. | |||

### The 'match' method resamples $data to the rotated scientific | ### The 'match' method resamples $data to the rotated scientific | |||

### coordinate system in $hdr. The "pixel coordinates" window shows | ### coordinate system in $hdr. The "pixel coordinates" window shows | |||

### the resampled data in their new pixel coordinate system. | ### the resampled data in their new pixel coordinate system. | |||

### The "sci. coordinates" window shows the data remaining fixed in | ### The "sci. coordinates" window shows the data remaining fixed in | |||

### scientific space, even though the pixels that represent them are | ### scientific space, even though the pixels that represent them are | |||

### moving and rotating. | ### moving and rotating. | |||

$hdr = $data->hdr_copy; | $hdr = $data->hdr_copy; | |||

for( $rot=0; $rot<=360; $rot += 10 ) { | for( $rot=0; $rot<=360; $rot += 10 ) { | |||

$hdr->{CROTA2} = $rot; | $hdr->{CROTA2} = $rot; | |||

$d = $data->match($hdr); | $d = $data->match($hdr); | |||

$w1->imag( $d, 600, 750 ); | $w1->imag( $d, 600, 750 ); | |||

$w2->fits_imag($d, 600, 750, {dr=>0}); | $w2->fits_imag($d, 600, 750, {dr=>0}); | |||

} | } | |||

|; | |], | |||

act q| | [act => q| | |||

### You can do the same thing even with nonsquare coordinates. | ### You can do the same thing even with nonsquare coordinates. | |||

### Here, we resample the same region in scientific space into a | ### Here, we resample the same region in scientific space into a | |||

### 150x50 pixel array. | ### 150x50 pixel array. | |||

$data = $m51->match([150,50],{or=>[[-0.05,0.15],[-0.05,0.15]]}); | $data = $m51->match([150,50],{or=>[[-0.05,0.15],[-0.05,0.15]]}); | |||

$hdr = $data->hdr_copy; | $hdr = $data->hdr_copy; | |||

$w1->release; | $w1->release; | |||

$w1->imag( $data, 600, 750, { title=>"${s}pixel${ss}", | $w1->imag( $data, 600, 750, { title=>"${s}pixel${ss}", | |||

xtitle=>"X$ps", ytitle=>"Y$ps", pix=>1 } ); | xtitle=>"X$ps", ytitle=>"Y$ps", pix=>1 } ); | |||

$w1->hold; | $w1->hold; | |||

for( $rot=0; $rot<=750; $rot += 5 ) { | for( $rot=0; $rot<=750; $rot += 5 ) { | |||

$hdr->{CROTA2} = $rot; | $hdr->{CROTA2} = $rot; | |||

$d = $data->match($hdr); | $d = $data->match($hdr); | |||

$w1->imag($d, 600, 750); $w2->fits_imag($d, 600, 750, {dr=>0}); | $w1->imag($d, 600, 750); $w2->fits_imag($d, 600, 750, {dr=>0}); | |||

} | } | |||

|], | ||||

|; | [comment => q| | |||

comment q| | ||||

This concludes the PDL::Transform demo. | This concludes the PDL::Transform demo. | |||

Be sure to check the documentation for PDL::Transform::Cartography, | Be sure to check the documentation for PDL::Transform::Cartography, | |||

which contains common perspective and mapping coordinate systems | which contains common perspective and mapping coordinate systems | |||

that are useful for work on the terrestrial and celestial spheres, | that are useful for work on the terrestrial and celestial spheres, | |||

as well as other planets &c. | as well as other planets &c. | |||

|], | ||||

); | ||||

|; | sub demo { @demo } | |||

sub done {' | ||||

$w1->release; $w1->close; undef $w1; | $w1->release; $w1->close; undef $w1; | |||

$w2->release; $w2->close; undef $w2; | $w2->release; $w2->close; undef $w2; | |||

undef $win; | undef $win; | |||

} | '} | |||

1; | 1; | |||

End of changes. 46 change blocks. | ||||

64 lines changed or deleted | | 53 lines changed or added |