flopy/flopy/modflow/mfdis.py

1087 lines
31 KiB
Python

"""
mfdis module. Contains the ModflowDis class. Note that the user can access
the ModflowDis class as `flopy.modflow.ModflowDis`.
Additional information for this MODFLOW package can be found at the `Online
MODFLOW Guide
<http://water.usgs.gov/ogw/modflow/MODFLOW-2005-Guide/index.html?dis.htm>`_.
"""
import sys
import warnings
import numpy as np
from ..pakbase import Package
from ..utils import Util2d, Util3d
from ..utils.reference import SpatialReference, TemporalReference
from ..utils.flopy_io import line_parse
ITMUNI = {"u": 0, "s": 1, "m": 2, "h": 3, "d": 4, "y": 5}
LENUNI = {"u": 0, "f": 1, "m": 2, "c": 3}
class ModflowDis(Package):
"""
MODFLOW Discretization Package Class.
Parameters
----------
model : model object
The model object (of type :class:`flopy.modflow.Modflow`) to which
this package will be added.
nlay : int
Number of model layers (the default is 1).
nrow : int
Number of model rows (the default is 2).
ncol : int
Number of model columns (the default is 2).
nper : int
Number of model stress periods (the default is 1).
delr : float or array of floats (ncol), optional
An array of spacings along a row (the default is 1.0).
delc : float or array of floats (nrow), optional
An array of spacings along a column (the default is 0.0).
laycbd : int or array of ints (nlay), optional
An array of flags indicating whether or not a layer has a Quasi-3D
confining bed below it. 0 indicates no confining bed, and not zero
indicates a confining bed. LAYCBD for the bottom layer must be 0. (the
default is 0)
top : float or array of floats (nrow, ncol), optional
An array of the top elevation of layer 1. For the common situation in
which the top layer represents a water-table aquifer, it may be
reasonable to set Top equal to land-surface elevation (the default is
1.0)
botm : float or array of floats (nlay, nrow, ncol), optional
An array of the bottom elevation for each model cell (the default is
0.)
perlen : float or array of floats (nper)
An array of the stress period lengths.
nstp : int or array of ints (nper)
Number of time steps in each stress period (default is 1).
tsmult : float or array of floats (nper)
Time step multiplier (default is 1.0).
steady : bool or array of bool (nper)
true or False indicating whether or not stress period is steady state
(default is True).
itmuni : int
Time units, default is days (4)
lenuni : int
Length units, default is meters (2)
extension : string
Filename extension (default is 'dis')
unitnumber : int
File unit number (default is None).
filenames : str or list of str
Filenames to use for the package. If filenames=None the package name
will be created using the model name and package extension. If a
single string is passed the package will be set to the string.
Default is None.
xul : float
x coordinate of upper left corner of the grid, default is None, which
means xul will be set to zero.
yul : float
y coordinate of upper-left corner of the grid, default is None, which
means yul will be calculated as the sum of the delc array. This
default, combined with the xul and rotation defaults will place the
lower-left corner of the grid at (0, 0).
rotation : float
counter-clockwise rotation (in degrees) of the grid about the lower-
left corner. default is 0.0
proj4_str : str
PROJ4 string that defines the projected coordinate system
(e.g. '+proj=utm +zone=14 +datum=WGS84 +units=m +no_defs ').
Can be an EPSG code (e.g. 'EPSG:32614'). Default is None.
start_datetime : str
starting datetime of the simulation. default is '1/1/1970'
Attributes
----------
heading : str
Text string written to top of package input file.
Methods
-------
See Also
--------
Notes
-----
Examples
--------
>>> import flopy
>>> m = flopy.modflow.Modflow()
>>> dis = flopy.modflow.ModflowDis(m)
"""
def __init__(
self,
model,
nlay=1,
nrow=2,
ncol=2,
nper=1,
delr=1.0,
delc=1.0,
laycbd=0,
top=1,
botm=0,
perlen=1,
nstp=1,
tsmult=1,
steady=True,
itmuni=4,
lenuni=2,
extension="dis",
unitnumber=None,
filenames=None,
xul=None,
yul=None,
rotation=None,
proj4_str=None,
start_datetime=None,
):
# set default unit number of one is not specified
if unitnumber is None:
unitnumber = ModflowDis._defaultunit()
# set filenames
if filenames is None:
filenames = [None]
elif isinstance(filenames, str):
filenames = [filenames]
# Fill namefile items
name = [ModflowDis._ftype()]
units = [unitnumber]
extra = [""]
# set package name
fname = [filenames[0]]
# Call ancestor's init to set self.parent, extension, name and unit number
Package.__init__(
self,
model,
extension=extension,
name=name,
unit_number=units,
extra=extra,
filenames=fname,
)
self.url = "dis.htm"
self.nrow = nrow
self.ncol = ncol
self.nlay = nlay
self.nper = nper
# initialize botm to an appropriate sized
if nlay > 1:
if isinstance(botm, float) or isinstance(botm, int):
botm = np.linspace(top, botm, nlay)
# Set values of all parameters
self.heading = (
"# {} package for ".format(self.name[0])
+ " {}, ".format(model.version_types[model.version])
+ "generated by Flopy."
)
self.laycbd = Util2d(
model, (self.nlay,), np.int32, laycbd, name="laycbd"
)
self.laycbd[-1] = 0 # bottom layer must be zero
self.delr = Util2d(
model,
(self.ncol,),
np.float32,
delr,
name="delr",
locat=self.unit_number[0],
)
self.delc = Util2d(
model,
(self.nrow,),
np.float32,
delc,
name="delc",
locat=self.unit_number[0],
)
self.top = Util2d(
model,
(self.nrow, self.ncol),
np.float32,
top,
name="model_top",
locat=self.unit_number[0],
)
self.botm = Util3d(
model,
(self.nlay + sum(self.laycbd), self.nrow, self.ncol),
np.float32,
botm,
"botm",
locat=self.unit_number[0],
)
self.perlen = Util2d(
model, (self.nper,), np.float32, perlen, name="perlen"
)
self.nstp = Util2d(model, (self.nper,), np.int32, nstp, name="nstp")
self.tsmult = Util2d(
model, (self.nper,), np.float32, tsmult, name="tsmult"
)
self.steady = Util2d(model, (self.nper,), bool, steady, name="steady")
try:
self.itmuni = int(itmuni)
except:
self.itmuni = ITMUNI[itmuni.lower()[0]]
try:
self.lenuni = int(lenuni)
except:
self.lenuni = LENUNI[lenuni.lower()[0]]
self.parent.add_package(self)
self.itmuni_dict = {
0: "undefined",
1: "seconds",
2: "minutes",
3: "hours",
4: "days",
5: "years",
}
if xul is None:
xul = model._xul
if yul is None:
yul = model._yul
if rotation is None:
rotation = model._rotation
if proj4_str is None:
proj4_str = model._proj4_str
if start_datetime is None:
start_datetime = model._start_datetime
# set the model grid coordinate info
xll = None
yll = None
mg = model.modelgrid
if rotation is not None:
mg.set_coord_info(xoff=None, yoff=None, angrot=rotation)
if xul is not None:
xll = mg._xul_to_xll(xul)
if yul is not None:
yll = mg._yul_to_yll(yul)
mg.set_coord_info(xoff=xll, yoff=yll, angrot=rotation, proj4=proj4_str)
xll = mg.xoffset
yll = mg.yoffset
rotation = mg.angrot
with warnings.catch_warnings():
warnings.simplefilter("ignore", category=DeprecationWarning)
self._sr = SpatialReference(
self.delr,
self.delc,
self.lenuni,
xll=xll,
yll=yll,
rotation=rotation or 0.0,
proj4_str=proj4_str,
)
self.tr = TemporalReference(
itmuni=self.itmuni, start_datetime=start_datetime
)
self.start_datetime = start_datetime
# calculate layer thicknesses
self.__calculate_thickness()
@property
def sr(self):
warnings.warn(
"SpatialReference has been deprecated. Use Grid instead.",
DeprecationWarning,
)
return self._sr
@sr.setter
def sr(self, sr):
warnings.warn(
"SpatialReference has been deprecated. Use Grid instead.",
DeprecationWarning,
)
self._sr = sr
def checklayerthickness(self):
"""
Check layer thickness.
"""
return (self.thickness > 0).all()
def get_totim(self):
"""
Get the totim at the end of each time step
Returns
-------
totim: numpy array
numpy array with simulation totim at the end of each time step
"""
totim = []
nstp = self.nstp.array
perlen = self.perlen.array
tsmult = self.tsmult.array
t = 0.0
for kper in range(self.nper):
m = tsmult[kper]
p = float(nstp[kper])
dt = perlen[kper]
if m > 1:
dt *= (m - 1.0) / (m ** p - 1.0)
else:
dt = dt / p
for kstp in range(nstp[kper]):
t += dt
totim.append(t)
if m > 1:
dt *= m
return np.array(totim, dtype=float)
def get_final_totim(self):
"""
Get the totim at the end of the simulation
Returns
-------
totim: float
maximum simulation totim
"""
return self.get_totim()[-1]
def get_kstp_kper_toffset(self, t=0.0):
"""
Get the stress period, time step, and time offset from passed time.
Parameters
----------
t : float
totim to return the stress period, time step, and toffset for
based on time discretization data. Default is 0.
Returns
-------
kstp : int
time step in stress period corresponding to passed totim
kper : int
stress period corresponding to passed totim
toffset : float
time offset of passed totim from the beginning of kper
"""
if t < 0.0:
t = 0.0
totim = self.get_totim()
nstp = self.nstp.array
ipos = 0
t0 = 0.0
kper = self.nper - 1
kstp = nstp[-1] - 1
toffset = self.perlen.array[-1]
done = False
for iper in range(self.nper):
tp0 = t0
for istp in range(nstp[iper]):
t1 = totim[ipos]
if t >= t0 and t < t1:
done = True
kper = iper
kstp = istp
toffset = t - tp0
break
ipos += 1
t0 = t1
if done:
break
return kstp, kper, toffset
def get_totim_from_kper_toffset(self, kper=0, toffset=0.0):
"""
Get totim from a passed kper and time offset from the beginning
of a stress period
Parameters
----------
kper : int
stress period. Default is 0
toffset : float
time offset relative to the beginning of kper
Returns
-------
t : float
totim to return the stress period, time step, and toffset for
based on time discretization data. Default is 0.
"""
if kper < 0:
kper = 0.0
if kper >= self.nper:
msg = (
"kper ({}) ".format(kper)
+ "must be less than "
+ "to nper ({}).".format(self.nper)
)
raise ValueError()
totim = self.get_totim()
nstp = self.nstp.array
ipos = 0
t0 = 0.0
tp0 = 0.0
for iper in range(kper + 1):
tp0 = t0
if iper == kper:
break
for istp in range(nstp[iper]):
t1 = totim[ipos]
ipos += 1
t0 = t1
t = tp0 + toffset
return t
def get_cell_volumes(self):
"""
Get an array of cell volumes.
Returns
-------
vol : array of floats (nlay, nrow, ncol)
"""
vol = np.empty((self.nlay, self.nrow, self.ncol))
for l in range(self.nlay):
vol[l, :, :] = self.thickness.array[l]
for r in range(self.nrow):
vol[:, r, :] *= self.delc[r]
for c in range(self.ncol):
vol[:, :, c] *= self.delr[c]
return vol
@property
def zcentroids(self):
z = np.empty((self.nlay, self.nrow, self.ncol))
z[0, :, :] = (self.top[:, :] + self.botm[0, :, :]) / 2.0
for l in range(1, self.nlay):
z[l, :, :] = (self.botm[l - 1, :, :] + self.botm[l, :, :]) / 2.0
return z
def get_node_coordinates(self):
"""
Get y, x, and z cell centroids in local model coordinates.
Returns
-------
y : list of cell y-centroids
x : list of cell x-centroids
z : array of floats (nlay, nrow, ncol)
"""
delr = self.delr.array
delc = self.delc.array
# In row direction
Ly = np.add.reduce(delc)
y = Ly - (np.add.accumulate(self.delc) - 0.5 * delc)
# In column direction
x = np.add.accumulate(self.delr) - 0.5 * delr
# In layer direction
z = self.zcentroids
return y, x, z
def get_rc_from_node_coordinates(self, x, y, local=True):
"""
Get the row and column of a point or sequence of points
in model coordinates.
Parameters
----------
x : float or sequence of floats
x coordinate(s) of points to find in model grid
y : float or sequence floats
y coordinate(s) of points to find in model grid
local : bool
x and y coordinates are in model local coordinates. If false, then
x and y are in world coordinates. (default is True)
Returns
-------
r : row or sequence of rows (zero-based)
c : column or sequence of columns (zero-based)
"""
mg = self.parent.modelgrid
if np.isscalar(x):
r, c = mg.intersect(x, y, local=local)
else:
r = []
c = []
for xx, yy in zip(x, y):
rr, cc = mg.intersect(xx, yy, local=local)
r.append(rr)
c.append(cc)
return r, c
def get_lrc(self, nodes):
"""
Get zero-based layer, row, column from a list of zero-based
MODFLOW node numbers.
Returns
-------
v : list of tuples containing the layer (k), row (i),
and column (j) for each node in the input list
"""
return self.parent.modelgrid.get_lrc(nodes)
def get_node(self, lrc_list):
"""
Get zero-based node number from a list of zero-based MODFLOW
layer, row, column tuples.
Returns
-------
v : list of MODFLOW nodes for each layer (k), row (i),
and column (j) tuple in the input list
"""
return self.parent.modelgrid.get_node(lrc_list)
def get_layer(self, i, j, elev):
"""Return the layer for an elevation at an i, j location.
Parameters
----------
i : row index (zero-based)
j : column index
elev : elevation (in same units as model)
Returns
-------
k : zero-based layer index
"""
return get_layer(self, i, j, elev)
def gettop(self):
"""
Get the top array.
Returns
-------
top : array of floats (nrow, ncol)
"""
return self.top.array
def getbotm(self, k=None):
"""
Get the bottom array.
Returns
-------
botm : array of floats (nlay, nrow, ncol), or
botm : array of floats (nrow, ncol) if k is not none
"""
if k is None:
return self.botm.array
else:
return self.botm.array[k, :, :]
def __calculate_thickness(self):
thk = []
thk.append(self.top - self.botm[0])
for k in range(1, self.nlay + sum(self.laycbd)):
thk.append(self.botm[k - 1] - self.botm[k])
self.__thickness = Util3d(
self.parent,
(self.nlay + sum(self.laycbd), self.nrow, self.ncol),
np.float32,
thk,
name="thickness",
)
@property
def thickness(self):
"""
Get a Util3d array of cell thicknesses.
Returns
-------
thickness : util3d array of floats (nlay, nrow, ncol)
"""
# return self.__thickness
thk = []
thk.append(self.top - self.botm[0])
for k in range(1, self.nlay + sum(self.laycbd)):
thk.append(self.botm[k - 1] - self.botm[k])
return Util3d(
self.parent,
(self.nlay + sum(self.laycbd), self.nrow, self.ncol),
np.float32,
thk,
name="thickness",
)
def write_file(self, check=True):
"""
Write the package file.
Parameters
----------
check : bool
Check package data for common errors. (default True)
Returns
-------
None
"""
if (
check
): # allows turning off package checks when writing files at model level
self.check(
f="{}.chk".format(self.name[0]),
verbose=self.parent.verbose,
level=1,
)
# Open file for writing
f_dis = open(self.fn_path, "w")
# Item 0: heading
f_dis.write("{0:s}\n".format(self.heading))
# f_dis.write('#{0:s}'.format(str(self.sr)))
# f_dis.write(" ,{0:s}:{1:s}\n".format("start_datetime",
# self.start_datetime))
# Item 1: NLAY, NROW, NCOL, NPER, ITMUNI, LENUNI
f_dis.write(
"{0:10d}{1:10d}{2:10d}{3:10d}{4:10d}{5:10d}\n".format(
self.nlay,
self.nrow,
self.ncol,
self.nper,
self.itmuni,
self.lenuni,
)
)
# Item 2: LAYCBD
for l in range(0, self.nlay):
f_dis.write("{0:3d}".format(self.laycbd[l]))
f_dis.write("\n")
# Item 3: DELR
f_dis.write(self.delr.get_file_entry())
# Item 4: DELC
f_dis.write(self.delc.get_file_entry())
# Item 5: Top(NCOL, NROW)
f_dis.write(self.top.get_file_entry())
# Item 5: BOTM(NCOL, NROW)
f_dis.write(self.botm.get_file_entry())
# Item 6: NPER, NSTP, TSMULT, Ss/tr
for t in range(self.nper):
f_dis.write(
"{0:14f}{1:14d}{2:10f} ".format(
self.perlen[t], self.nstp[t], self.tsmult[t]
)
)
if self.steady[t]:
f_dis.write(" {0:3s}\n".format("SS"))
else:
f_dis.write(" {0:3s}\n".format("TR"))
f_dis.close()
def check(self, f=None, verbose=True, level=1, checktype=None):
"""
Check dis package data for zero and negative thicknesses.
Parameters
----------
f : str or file handle
String defining file name or file handle for summary file
of check method output. If a sting is passed a file handle
is created. If f is None, check method does not write
results to a summary file. (default is None)
verbose : bool
Boolean flag used to determine if check method results are
written to the screen
level : int
Check method analysis level. If level=0, summary checks are
performed. If level=1, full checks are performed.
Returns
-------
None
Examples
--------
>>> import flopy
>>> m = flopy.modflow.Modflow.load('model.nam')
>>> m.dis.check()
"""
chk = self._get_check(f, verbose, level, checktype)
# make ibound of same shape as thicknesses/botm for quasi-3D models
active = chk.get_active(include_cbd=True)
# Use either a numpy array or masked array
thickness = self.thickness.array
non_finite = ~(np.isfinite(thickness))
if non_finite.any():
thickness[non_finite] = 0
thickness = np.ma.array(thickness, mask=non_finite)
chk.values(
thickness,
active & (thickness <= 0),
"zero or negative thickness",
"Error",
)
thin_cells = (thickness < chk.thin_cell_threshold) & (thickness > 0)
chk.values(
thickness,
active & thin_cells,
"thin cells (less than checker threshold of {:.1f})".format(
chk.thin_cell_threshold
),
"Error",
)
chk.values(
self.top.array,
active[0, :, :] & np.isnan(self.top.array),
"nan values in top array",
"Error",
)
chk.values(
self.botm.array,
active & np.isnan(self.botm.array),
"nan values in bottom array",
"Error",
)
chk.summarize()
return chk
@classmethod
def load(cls, f, model, ext_unit_dict=None, check=True):
"""
Load an existing package.
Parameters
----------
f : filename or file handle
File to load.
model : model object
The model object (of type :class:`flopy.modflow.mf.Modflow`) to
which this package will be added.
ext_unit_dict : dictionary, optional
If the arrays in the file are specified using EXTERNAL,
or older style array control records, then `f` should be a file
handle. In this case ext_unit_dict is required, which can be
constructed using the function
:class:`flopy.utils.mfreadnam.parsenamefile`.
check : bool
Check package data for common errors. (default True)
Returns
-------
dis : ModflowDis object
ModflowDis object.
Examples
--------
>>> import flopy
>>> m = flopy.modflow.Modflow()
>>> dis = flopy.modflow.ModflowDis.load('test.dis', m)
"""
if model.verbose:
sys.stdout.write("loading dis package file...\n")
openfile = not hasattr(f, "read")
if openfile:
filename = f
f = open(filename, "r")
# dataset 0 -- header
header = ""
while True:
line = f.readline()
if line[0] != "#":
break
header += line.strip()
header = header.replace("#", "")
xul, yul = None, None
rotation = None
proj4_str = None
start_datetime = "1/1/1970"
dep = False
for item in header.split(","):
if "xul" in item.lower():
try:
xul = float(item.split(":")[1])
except:
if model.verbose:
print(
" could not parse xul "
+ "in {}".format(filename)
)
dep = True
elif "yul" in item.lower():
try:
yul = float(item.split(":")[1])
except:
if model.verbose:
print(
" could not parse yul "
+ "in {}".format(filename)
)
dep = True
elif "rotation" in item.lower():
try:
rotation = float(item.split(":")[1])
except:
if model.verbose:
print(
" could not parse rotation "
+ "in {}".format(filename)
)
dep = True
elif "proj4_str" in item.lower():
try:
proj4_str = ":".join(item.split(":")[1:]).strip()
except:
if model.verbose:
print(
" could not parse proj4_str "
+ "in {}".format(filename)
)
dep = True
elif "start" in item.lower():
try:
start_datetime = item.split(":")[1].strip()
except:
if model.verbose:
print(
" could not parse start "
+ "in {}".format(filename)
)
dep = True
if dep:
warnings.warn(
"SpatialReference information found in DIS header,"
"this information is being ignored. "
"SpatialReference info is now stored in the namfile"
"header"
)
# dataset 1
nlay, nrow, ncol, nper, itmuni, lenuni = line.strip().split()[0:6]
nlay = int(nlay)
nrow = int(nrow)
ncol = int(ncol)
nper = int(nper)
itmuni = int(itmuni)
lenuni = int(lenuni)
# dataset 2 -- laycbd
if model.verbose:
print(
" Loading dis package with:\n "
+ "{0} layers, {1} rows, {2} columns, and {3} stress periods".format(
nlay, nrow, ncol, nper
)
)
print(" loading laycbd...")
laycbd = np.zeros(nlay, dtype=int)
d = 0
while True:
line = f.readline()
raw = line.strip("\n").split()
for val in raw:
if int(val) != 0:
laycbd[d] = 1
d += 1
if d == nlay:
break
if d == nlay:
break
# dataset 3 -- delr
if model.verbose:
print(" loading delr...")
delr = Util2d.load(
f, model, (ncol,), np.float32, "delr", ext_unit_dict
)
# dataset 4 -- delc
if model.verbose:
print(" loading delc...")
delc = Util2d.load(
f, model, (nrow,), np.float32, "delc", ext_unit_dict
)
# dataset 5 -- top
if model.verbose:
print(" loading top...")
top = Util2d.load(
f, model, (nrow, ncol), np.float32, "top", ext_unit_dict
)
# dataset 6 -- botm
ncbd = laycbd.sum()
if model.verbose:
print(" loading botm...")
print(
" for {} layers and ".format(nlay)
+ "{} confining beds".format(ncbd)
)
if nlay > 1:
botm = Util3d.load(
f,
model,
(nlay + ncbd, nrow, ncol),
np.float32,
"botm",
ext_unit_dict,
)
else:
botm = Util3d.load(
f, model, (nlay, nrow, ncol), np.float32, "botm", ext_unit_dict
)
# dataset 7 -- stress period info
if model.verbose:
print(" loading stress period data...")
print(" for {} stress periods".format(nper))
perlen = []
nstp = []
tsmult = []
steady = []
for k in range(nper):
line = f.readline()
a1, a2, a3, a4 = line_parse(line)[0:4]
a1 = float(a1)
a2 = int(a2)
a3 = float(a3)
if a4.upper() == "TR":
a4 = False
else:
a4 = True
perlen.append(a1)
nstp.append(a2)
tsmult.append(a3)
steady.append(a4)
if openfile:
f.close()
# set package unit number
unitnumber = None
filenames = [None]
if ext_unit_dict is not None:
unitnumber, filenames[0] = model.get_ext_dict_attr(
ext_unit_dict, filetype=ModflowDis._ftype()
)
# create dis object instance
dis = cls(
model,
nlay=nlay,
nrow=nrow,
ncol=ncol,
nper=nper,
delr=delr,
delc=delc,
laycbd=laycbd,
top=top,
botm=botm,
perlen=perlen,
nstp=nstp,
tsmult=tsmult,
steady=steady,
itmuni=itmuni,
lenuni=lenuni,
xul=xul,
yul=yul,
rotation=rotation,
proj4_str=proj4_str,
start_datetime=start_datetime,
unitnumber=unitnumber,
filenames=filenames,
)
if check:
dis.check(
f="{}.chk".format(dis.name[0]),
verbose=dis.parent.verbose,
level=0,
)
# return dis object instance
return dis
@staticmethod
def _ftype():
return "DIS"
@staticmethod
def _defaultunit():
return 11
def get_layer(dis, i, j, elev):
"""Return the layers for elevations at i, j locations.
Parameters
----------
dis : flopy.modflow.ModflowDis object
i : scaler or sequence
row index (zero-based)
j : scaler or sequence
column index
elev : scaler or sequence
elevation (in same units as model)
Returns
-------
k : np.ndarray (1-D) or scalar
zero-based layer index
"""
def to_array(arg):
if not isinstance(arg, np.ndarray):
return np.array([arg])
else:
return arg
i = to_array(i)
j = to_array(j)
elev = to_array(elev)
botms = dis.botm.array[:, i, j].tolist()
layers = np.sum(((botms - elev) > 0), axis=0)
# force elevations below model bottom into bottom layer
layers[layers > dis.nlay - 1] = dis.nlay - 1
layers = np.atleast_1d(np.squeeze(layers))
if len(layers) == 1:
layers = layers[0]
return layers