From 3accf62db0c9616a1567ac4d2ca5d6b25db08f0d Mon Sep 17 00:00:00 2001
From: mjreno <mjreno@IGSAAA071L01144.gs.doi.net>
Date: Thu, 18 Jul 2024 16:48:06 -0400
Subject: [PATCH 1/4] basic dfn type based on toml specification

---
 dfn/prt-prp.dfn       | 405 +++++++++++++++++++++++
 dfn/toml/prt-prp.toml | 726 ++++++++++++++++++++++++++++++++++++++++++
 flopy4/dfn.py         |  95 ++++++
 pyproject.toml        |   3 +-
 test/test_dfn.py      |  95 ++++++
 5 files changed, 1323 insertions(+), 1 deletion(-)
 create mode 100644 dfn/prt-prp.dfn
 create mode 100644 dfn/toml/prt-prp.toml
 create mode 100644 flopy4/dfn.py
 create mode 100644 test/test_dfn.py

diff --git a/dfn/prt-prp.dfn b/dfn/prt-prp.dfn
new file mode 100644
index 0000000..6affe2c
--- /dev/null
+++ b/dfn/prt-prp.dfn
@@ -0,0 +1,405 @@
+# --------------------- prt prp options ---------------------
+# flopy multi-package
+
+block options
+name boundnames
+type keyword
+shape
+reader urword
+optional true
+longname
+description keyword to indicate that boundary names may be provided with the list of particle release points.
+
+block options
+name print_input
+type keyword
+reader urword
+optional true
+longname print input to listing file
+description REPLACE print_input {'{#1}': 'all model stress package'}
+
+block options
+name dev_exit_solve_method
+type integer
+reader urword
+optional true
+longname exit solve method
+description the method for iterative solution of particle exit location and time in the generalized Pollock's method.  0 default, 1 Brent, 2 Chandrupatla.  The default is Brent's method.
+
+block options
+name exit_solve_tolerance
+type double precision
+reader urword
+optional false
+longname exit solve tolerance
+description the convergence tolerance for iterative solution of particle exit location and time in the generalized Pollock's method.  A value of 0.00001 works well for many problems, but the value that strikes the best balance between accuracy and runtime is problem-dependent.
+
+block options
+name local_z
+type keyword
+reader urword
+optional true
+longname whether to use local z coordinates
+description indicates that ``zrpt'' defines the local z coordinate of the release point within the cell, with value of 0 at the bottom and 1 at the top of the cell.  If the cell is partially saturated at release time, the top of the cell is considered to be the water table elevation (the head in the cell) rather than the top defined by the user.
+
+block options
+name extend_tracking
+type keyword
+reader urword
+optional true
+longname whether to extend tracking beyond the end of the simulation
+description indicates that particles should be tracked beyond the end of the simulation's final time step (using that time step's flows) until particles terminate or reach a specified stop time.  By default, particles are terminated at the end of the simulation's final time step.
+
+block options
+name track_filerecord
+type record track fileout trackfile
+shape
+reader urword
+tagged true
+optional true
+longname
+description
+
+block options
+name track
+type keyword
+shape
+in_record true
+reader urword
+tagged true
+optional false
+longname track keyword
+description keyword to specify that record corresponds to a binary track output file.  Each PRP Package may have a distinct binary track output file.
+
+block options
+name fileout
+type keyword
+shape
+in_record true
+reader urword
+tagged true
+optional false
+longname file keyword
+description keyword to specify that an output filename is expected next.
+
+block options
+name trackfile
+type string
+preserve_case true
+shape
+in_record true
+reader urword
+tagged false
+optional false
+longname file keyword
+description name of the binary output file to write tracking information.
+
+block options
+name trackcsv_filerecord
+type record trackcsv fileout trackcsvfile
+shape
+reader urword
+tagged true
+optional true
+longname
+description
+
+block options
+name trackcsv
+type keyword
+shape
+in_record true
+reader urword
+tagged true
+optional false
+longname track keyword
+description keyword to specify that record corresponds to a CSV track output file.  Each PRP Package may have a distinct CSV track output file.
+
+block options
+name trackcsvfile
+type string
+preserve_case true
+shape
+in_record true
+reader urword
+tagged false
+optional false
+longname file keyword
+description name of the comma-separated value (CSV) file to write tracking information.
+
+block options
+name stoptime
+type double precision
+reader urword
+optional true
+longname stop time
+description real value defining the maximum simulation time to which particles in the package can be tracked.  Particles that have not terminated earlier due to another termination condition will terminate when simulation time STOPTIME is reached.  If the last stress period in the simulation consists of more than one time step, particles will not be tracked past the ending time of the last stress period, regardless of STOPTIME.  If the last stress period in the simulation consists of a single time step, it is assumed to be a steady-state stress period, and its ending time will not limit the simulation time to which particles can be tracked.  If STOPTIME and STOPTRAVELTIME are both provided, particles will be stopped if either is reached.
+
+block options
+name stoptraveltime
+type double precision
+reader urword
+optional true
+longname stop travel time
+description real value defining the maximum travel time over which particles in the model can be tracked.  Particles that have not terminated earlier due to another termination condition will terminate when their travel time reaches STOPTRAVELTIME.  If the last stress period in the simulation consists of more than one time step, particles will not be tracked past the ending time of the last stress period, regardless of STOPTRAVELTIME.  If the last stress period in the simulation consists of a single time step, it is assumed to be a steady-state stress period, and its ending time will not limit the travel time over which particles can be tracked.  If STOPTIME and STOPTRAVELTIME are both provided, particles will be stopped if either is reached.
+
+block options
+name stop_at_weak_sink
+type keyword
+reader urword
+optional true
+longname stop at weak sink
+description is a text keyword to indicate that a particle is to terminate when it enters a cell that is a weak sink.  By default, particles are allowed to pass though cells that are weak sinks.
+
+block options
+name istopzone
+type integer
+reader urword
+optional true
+longname stop zone number
+description integer value defining the stop zone number.  If cells have been assigned IZONE values in the GRIDDATA block, a particle terminates if it enters a cell whose IZONE value matches ISTOPZONE.  An ISTOPZONE value of zero indicates that there is no stop zone.  The default value is zero.
+
+block options
+name drape
+type keyword
+reader urword
+optional true
+longname drape
+description is a text keyword to indicate that if a particle's release point is in a cell that happens to be inactive at release time, the particle is to be moved to the topmost active cell below it, if any. By default, a particle is not released into the simulation if its release point's cell is inactive at release time.
+
+block options
+name release_timesrecord
+type record release_times times
+shape
+reader urword
+tagged true
+optional true
+longname
+description
+
+block options
+name release_times
+type keyword
+reader urword
+in_record true
+tagged true
+shape
+longname
+description keyword indicating release times will follow
+
+block options
+name times
+type double precision
+shape (unknown)
+reader urword
+in_record true
+tagged false
+repeating true
+longname release times
+description times to release, relative to the beginning of the simulation.  RELEASE\_TIMES and RELEASE\_TIMESFILE are mutually exclusive.
+
+block options
+name release_timesfilerecord
+type record release_timesfile timesfile
+shape
+reader urword
+tagged true
+optional true
+longname
+description
+
+block options
+name release_timesfile
+type keyword
+reader urword
+in_record true
+tagged true
+shape
+longname
+description keyword indicating release times file name will follow
+
+block options
+name timesfile
+type string
+preserve_case true
+shape
+in_record true
+reader urword
+tagged false
+optional false
+longname file keyword
+description name of the release times file.  RELEASE\_TIMES and RELEASE\_TIMESFILE are mutually exclusive.
+
+block options
+name dev_forceternary
+type keyword
+reader urword
+optional false
+longname force ternary tracking method
+description force use of the ternary tracking method regardless of cell type in DISV grids.
+mf6internal ifrctrn
+
+
+# --------------------- prt prp dimensions ---------------------
+
+block dimensions
+name nreleasepts
+type integer
+reader urword
+optional false
+longname number of particle release points
+description is the number of particle release points.
+
+# --------------------- prt prp packagedata ---------------------
+
+block packagedata
+name packagedata
+type recarray irptno cellid xrpt yrpt zrpt boundname
+shape (nreleasepts)
+reader urword
+longname
+description
+
+block packagedata
+name irptno
+type integer
+shape
+tagged false
+in_record true
+reader urword
+longname PRP id number for release point
+description integer value that defines the PRP release point number associated with the specified PACKAGEDATA data on the line. IRPTNO must be greater than zero and less than or equal to NRELEASEPTS.  The program will terminate with an error if information for a PRP release point number is specified more than once.
+numeric_index true
+
+block packagedata
+name cellid
+type integer
+shape (ncelldim)
+tagged false
+in_record true
+reader urword
+longname cell identifier
+description REPLACE cellid {}
+
+block packagedata
+name xrpt
+type double precision
+shape
+tagged false
+in_record true
+reader urword
+longname x coordinate of release point
+description real value that defines the x coordinate of the release point in model coordinates.  The (x, y, z) location specified for the release point must lie within the cell that is identified by the specified cellid.
+
+block packagedata
+name yrpt
+type double precision
+shape
+tagged false
+in_record true
+reader urword
+longname y coordinate of release point
+description real value that defines the y coordinate of the release point in model coordinates.  The (x, y, z) location specified for the release point must lie within the cell that is identified by the specified cellid.
+
+block packagedata
+name zrpt
+type double precision
+shape
+tagged false
+in_record true
+reader urword
+longname z coordinate of release point
+description real value that defines the z coordinate of the release point in model coordinates or, if the LOCAL\_Z option is active, in local cell coordinates.  The (x, y, z) location specified for the release point must lie within the cell that is identified by the specified cellid.
+
+block packagedata
+name boundname
+type string
+shape
+tagged false
+in_record true
+reader urword
+optional true
+longname release point name
+description name of the particle release point. BOUNDNAME is an ASCII character variable that can contain as many as 40 characters. If BOUNDNAME contains spaces in it, then the entire name must be enclosed within single quotes.
+
+# --------------------- prt prp period ---------------------
+
+block period
+name iper
+type integer
+block_variable True
+in_record true
+tagged false
+shape
+valid
+reader urword
+optional false
+longname stress period number
+description integer value specifying the stress period number for which the data specified in the PERIOD block apply. IPER must be less than or equal to NPER in the TDIS Package and greater than zero. The IPER value assigned to a stress period block must be greater than the IPER value assigned for the previous PERIOD block. The information specified in the PERIOD block applies only to that stress period.
+
+block period
+name perioddata
+type recarray releasesetting
+shape
+reader urword
+longname
+description
+
+block period
+name releasesetting
+type keystring all first frequency steps fraction
+shape
+tagged false
+in_record true
+reader urword
+longname
+description specifies when to release particles within the stress period.  Overrides package-level RELEASETIME option, which applies to all stress periods. By default, RELEASESETTING configures particles for release at the beginning of the specified time steps. For time-offset releases, provide a FRACTION value.
+
+block period
+name all
+type keyword
+shape
+in_record true
+reader urword
+longname
+description keyword to indicate release of particles at the start of all time steps in the period.
+
+block period
+name first
+type keyword
+shape
+in_record true
+reader urword
+longname
+description keyword to indicate release of particles at the start of the first time step in the period. This keyword may be used in conjunction with other keywords to release particles at the start of multiple time steps.
+
+block period
+name frequency
+type integer
+shape
+tagged true
+in_record true
+reader urword
+longname
+description release particles at the specified time step frequency. This keyword may be used in conjunction with other keywords to release particles at the start of multiple time steps.
+
+block period
+name steps
+type integer
+shape (<nstp)
+tagged true
+in_record true
+reader urword
+longname
+description release particles at the start of each step specified in STEPS. This keyword may be used in conjunction with other keywords to release particles at the start of multiple time steps.
+
+block period
+name fraction
+type double precision
+shape (<nstp)
+tagged true
+in_record true
+reader urword
+optional true
+longname
+description release particles after the specified fraction of the time step has elapsed. If FRACTION is not set, particles are released at the start of the specified time step(s). FRACTION must be a single value when used with ALL, FIRST, or FREQUENCY. When used with STEPS, FRACTION may be a single value or an array of the same length as STEPS. If a single FRACTION value is provided with STEPS, the fraction applies to all steps.
\ No newline at end of file
diff --git a/dfn/toml/prt-prp.toml b/dfn/toml/prt-prp.toml
new file mode 100644
index 0000000..4d56c6c
--- /dev/null
+++ b/dfn/toml/prt-prp.toml
@@ -0,0 +1,726 @@
+component = "PRT"
+subcomponent = "PRP"
+blocknames = [ "options", "dimensions", "packagedata", "period",]
+multi_package = true
+advanced = false
+subpackage = []
+
+[block.options.boundnames]
+type = "keyword"
+block_variable = false
+valid = []
+shape = ""
+tagged = true
+in_record = false
+layered = false
+time_series = false
+reader = "urword"
+optional = "true"
+preserve_case = false
+numeric_index = false
+mf6internal = "boundnames"
+longname = ""
+description = "keyword to indicate that boundary names may be provided with the list of particle release points."
+deprecated = ""
+
+[block.options.print_input]
+type = "keyword"
+block_variable = false
+valid = []
+shape = ""
+tagged = true
+in_record = false
+layered = false
+time_series = false
+reader = "urword"
+optional = "true"
+preserve_case = false
+numeric_index = false
+mf6internal = "print_input"
+longname = "print input to listing file"
+description = "REPLACE print_input {'{#1}': 'all model stress package'}"
+deprecated = ""
+
+[block.options.dev_exit_solve_method]
+type = "integer"
+block_variable = false
+valid = []
+shape = ""
+tagged = true
+in_record = false
+layered = false
+time_series = false
+reader = "urword"
+optional = "true"
+preserve_case = false
+numeric_index = false
+mf6internal = "dev_exit_solve_method"
+longname = "exit solve method"
+description = "the method for iterative solution of particle exit location and time in the generalized Pollock's method.  0 default, 1 Brent, 2 Chandrupatla.  The default is Brent's method."
+deprecated = ""
+
+[block.options.exit_solve_tolerance]
+type = "double precision"
+block_variable = false
+valid = []
+shape = ""
+tagged = true
+in_record = false
+layered = false
+time_series = false
+reader = "urword"
+optional = "false"
+preserve_case = false
+numeric_index = false
+mf6internal = "exit_solve_tolerance"
+longname = "exit solve tolerance"
+description = "the convergence tolerance for iterative solution of particle exit location and time in the generalized Pollock's method.  A value of 0.00001 works well for many problems, but the value that strikes the best balance between accuracy and runtime is problem-dependent."
+deprecated = ""
+
+[block.options.local_z]
+type = "keyword"
+block_variable = false
+valid = []
+shape = ""
+tagged = true
+in_record = false
+layered = false
+time_series = false
+reader = "urword"
+optional = "true"
+preserve_case = false
+numeric_index = false
+mf6internal = "local_z"
+longname = "whether to use local z coordinates"
+description = "indicates that ``zrpt'' defines the local z coordinate of the release point within the cell, with value of 0 at the bottom and 1 at the top of the cell.  If the cell is partially saturated at release time, the top of the cell is considered to be the water table elevation (the head in the cell) rather than the top defined by the user."
+deprecated = ""
+
+[block.options.extend_tracking]
+type = "keyword"
+block_variable = false
+valid = []
+shape = ""
+tagged = true
+in_record = false
+layered = false
+time_series = false
+reader = "urword"
+optional = "true"
+preserve_case = false
+numeric_index = false
+mf6internal = "extend_tracking"
+longname = "whether to extend tracking beyond the end of the simulation"
+description = "indicates that particles should be tracked beyond the end of the simulation's final time step (using that time step's flows) until particles terminate or reach a specified stop time.  By default, particles are terminated at the end of the simulation's final time step."
+deprecated = ""
+
+[block.options.track_filerecord]
+type = "record track fileout trackfile"
+block_variable = false
+valid = []
+shape = ""
+tagged = "true"
+in_record = false
+layered = false
+time_series = false
+reader = "urword"
+optional = "true"
+preserve_case = false
+numeric_index = false
+mf6internal = "track_filerecord"
+longname = ""
+description = ""
+deprecated = ""
+
+[block.options.track]
+type = "keyword"
+block_variable = false
+valid = []
+shape = ""
+tagged = "true"
+in_record = "true"
+layered = false
+time_series = false
+reader = "urword"
+optional = "false"
+preserve_case = false
+numeric_index = false
+mf6internal = "track"
+longname = "track keyword"
+description = "keyword to specify that record corresponds to a binary track output file.  Each PRP Package may have a distinct binary track output file."
+deprecated = ""
+
+[block.options.fileout]
+type = "keyword"
+block_variable = false
+valid = []
+shape = ""
+tagged = "true"
+in_record = "true"
+layered = false
+time_series = false
+reader = "urword"
+optional = "false"
+preserve_case = false
+numeric_index = false
+mf6internal = "fileout"
+longname = "file keyword"
+description = "keyword to specify that an output filename is expected next."
+deprecated = ""
+
+[block.options.trackfile]
+type = "string"
+block_variable = false
+valid = []
+shape = ""
+tagged = "false"
+in_record = "true"
+layered = false
+time_series = false
+reader = "urword"
+optional = "false"
+preserve_case = "true"
+numeric_index = false
+mf6internal = "trackfile"
+longname = "file keyword"
+description = "name of the binary output file to write tracking information."
+deprecated = ""
+
+[block.options.trackcsv_filerecord]
+type = "record trackcsv fileout trackcsvfile"
+block_variable = false
+valid = []
+shape = ""
+tagged = "true"
+in_record = false
+layered = false
+time_series = false
+reader = "urword"
+optional = "true"
+preserve_case = false
+numeric_index = false
+mf6internal = "trackcsv_filerecord"
+longname = ""
+description = ""
+deprecated = ""
+
+[block.options.trackcsv]
+type = "keyword"
+block_variable = false
+valid = []
+shape = ""
+tagged = "true"
+in_record = "true"
+layered = false
+time_series = false
+reader = "urword"
+optional = "false"
+preserve_case = false
+numeric_index = false
+mf6internal = "trackcsv"
+longname = "track keyword"
+description = "keyword to specify that record corresponds to a CSV track output file.  Each PRP Package may have a distinct CSV track output file."
+deprecated = ""
+
+[block.options.trackcsvfile]
+type = "string"
+block_variable = false
+valid = []
+shape = ""
+tagged = "false"
+in_record = "true"
+layered = false
+time_series = false
+reader = "urword"
+optional = "false"
+preserve_case = "true"
+numeric_index = false
+mf6internal = "trackcsvfile"
+longname = "file keyword"
+description = "name of the comma-separated value (CSV) file to write tracking information."
+deprecated = ""
+
+[block.options.stoptime]
+type = "double precision"
+block_variable = false
+valid = []
+shape = ""
+tagged = true
+in_record = false
+layered = false
+time_series = false
+reader = "urword"
+optional = "true"
+preserve_case = false
+numeric_index = false
+mf6internal = "stoptime"
+longname = "stop time"
+description = "real value defining the maximum simulation time to which particles in the package can be tracked.  Particles that have not terminated earlier due to another termination condition will terminate when simulation time STOPTIME is reached.  If the last stress period in the simulation consists of more than one time step, particles will not be tracked past the ending time of the last stress period, regardless of STOPTIME.  If the last stress period in the simulation consists of a single time step, it is assumed to be a steady-state stress period, and its ending time will not limit the simulation time to which particles can be tracked.  If STOPTIME and STOPTRAVELTIME are both provided, particles will be stopped if either is reached."
+deprecated = ""
+
+[block.options.stoptraveltime]
+type = "double precision"
+block_variable = false
+valid = []
+shape = ""
+tagged = true
+in_record = false
+layered = false
+time_series = false
+reader = "urword"
+optional = "true"
+preserve_case = false
+numeric_index = false
+mf6internal = "stoptraveltime"
+longname = "stop travel time"
+description = "real value defining the maximum travel time over which particles in the model can be tracked.  Particles that have not terminated earlier due to another termination condition will terminate when their travel time reaches STOPTRAVELTIME.  If the last stress period in the simulation consists of more than one time step, particles will not be tracked past the ending time of the last stress period, regardless of STOPTRAVELTIME.  If the last stress period in the simulation consists of a single time step, it is assumed to be a steady-state stress period, and its ending time will not limit the travel time over which particles can be tracked.  If STOPTIME and STOPTRAVELTIME are both provided, particles will be stopped if either is reached."
+deprecated = ""
+
+[block.options.stop_at_weak_sink]
+type = "keyword"
+block_variable = false
+valid = []
+shape = ""
+tagged = true
+in_record = false
+layered = false
+time_series = false
+reader = "urword"
+optional = "true"
+preserve_case = false
+numeric_index = false
+mf6internal = "stop_at_weak_sink"
+longname = "stop at weak sink"
+description = "is a text keyword to indicate that a particle is to terminate when it enters a cell that is a weak sink.  By default, particles are allowed to pass though cells that are weak sinks."
+deprecated = ""
+
+[block.options.istopzone]
+type = "integer"
+block_variable = false
+valid = []
+shape = ""
+tagged = true
+in_record = false
+layered = false
+time_series = false
+reader = "urword"
+optional = "true"
+preserve_case = false
+numeric_index = false
+mf6internal = "istopzone"
+longname = "stop zone number"
+description = "integer value defining the stop zone number.  If cells have been assigned IZONE values in the GRIDDATA block, a particle terminates if it enters a cell whose IZONE value matches ISTOPZONE.  An ISTOPZONE value of zero indicates that there is no stop zone.  The default value is zero."
+deprecated = ""
+
+[block.options.drape]
+type = "keyword"
+block_variable = false
+valid = []
+shape = ""
+tagged = true
+in_record = false
+layered = false
+time_series = false
+reader = "urword"
+optional = "true"
+preserve_case = false
+numeric_index = false
+mf6internal = "drape"
+longname = "drape"
+description = "is a text keyword to indicate that if a particle's release point is in a cell that happens to be inactive at release time, the particle is to be moved to the topmost active cell below it, if any. By default, a particle is not released into the simulation if its release point's cell is inactive at release time."
+deprecated = ""
+
+[block.options.release_timesrecord]
+type = "record release_times times"
+block_variable = false
+valid = []
+shape = ""
+tagged = "true"
+in_record = false
+layered = false
+time_series = false
+reader = "urword"
+optional = "true"
+preserve_case = false
+numeric_index = false
+mf6internal = "release_timesrecord"
+longname = ""
+description = ""
+deprecated = ""
+
+[block.options.release_times]
+type = "keyword"
+block_variable = false
+valid = []
+shape = ""
+tagged = "true"
+in_record = "true"
+layered = false
+time_series = false
+reader = "urword"
+optional = false
+preserve_case = false
+numeric_index = false
+mf6internal = "release_times"
+longname = ""
+description = "keyword indicating release times will follow"
+deprecated = ""
+
+[block.options.times]
+type = "double precision"
+block_variable = false
+valid = []
+shape = "(unknown)"
+tagged = "false"
+in_record = "true"
+layered = false
+time_series = false
+reader = "urword"
+optional = false
+preserve_case = false
+numeric_index = false
+mf6internal = "times"
+longname = "release times"
+description = "times to release, relative to the beginning of the simulation.  RELEASE\\_TIMES and RELEASE\\_TIMESFILE are mutually exclusive."
+deprecated = ""
+
+[block.options.release_timesfilerecord]
+type = "record release_timesfile timesfile"
+block_variable = false
+valid = []
+shape = ""
+tagged = "true"
+in_record = false
+layered = false
+time_series = false
+reader = "urword"
+optional = "true"
+preserve_case = false
+numeric_index = false
+mf6internal = "release_timesfilerecord"
+longname = ""
+description = ""
+deprecated = ""
+
+[block.options.release_timesfile]
+type = "keyword"
+block_variable = false
+valid = []
+shape = ""
+tagged = "true"
+in_record = "true"
+layered = false
+time_series = false
+reader = "urword"
+optional = false
+preserve_case = false
+numeric_index = false
+mf6internal = "release_timesfile"
+longname = ""
+description = "keyword indicating release times file name will follow"
+deprecated = ""
+
+[block.options.timesfile]
+type = "string"
+block_variable = false
+valid = []
+shape = ""
+tagged = "false"
+in_record = "true"
+layered = false
+time_series = false
+reader = "urword"
+optional = "false"
+preserve_case = "true"
+numeric_index = false
+mf6internal = "timesfile"
+longname = "file keyword"
+description = "name of the release times file.  RELEASE\\_TIMES and RELEASE\\_TIMESFILE are mutually exclusive."
+deprecated = ""
+
+[block.options.dev_forceternary]
+type = "keyword"
+block_variable = false
+valid = []
+shape = ""
+tagged = true
+in_record = false
+layered = false
+time_series = false
+reader = "urword"
+optional = "false"
+preserve_case = false
+numeric_index = false
+mf6internal = "ifrctrn"
+longname = "force ternary tracking method"
+description = "force use of the ternary tracking method regardless of cell type in DISV grids."
+deprecated = ""
+
+[block.dimensions.nreleasepts]
+type = "integer"
+block_variable = false
+valid = []
+shape = ""
+tagged = true
+in_record = false
+layered = false
+time_series = false
+reader = "urword"
+optional = "false"
+preserve_case = false
+numeric_index = false
+mf6internal = "nreleasepts"
+longname = "number of particle release points"
+description = "is the number of particle release points."
+deprecated = ""
+
+[block.packagedata.packagedata]
+type = "recarray irptno cellid xrpt yrpt zrpt boundname"
+block_variable = false
+valid = []
+shape = "(nreleasepts)"
+tagged = true
+in_record = false
+layered = false
+time_series = false
+reader = "urword"
+optional = false
+preserve_case = false
+numeric_index = false
+mf6internal = "packagedata"
+longname = ""
+description = ""
+deprecated = ""
+
+[block.packagedata.irptno]
+type = "integer"
+block_variable = false
+valid = []
+shape = ""
+tagged = "false"
+in_record = "true"
+layered = false
+time_series = false
+reader = "urword"
+optional = false
+preserve_case = false
+numeric_index = "true"
+mf6internal = "irptno"
+longname = "PRP id number for release point"
+description = "integer value that defines the PRP release point number associated with the specified PACKAGEDATA data on the line. IRPTNO must be greater than zero and less than or equal to NRELEASEPTS.  The program will terminate with an error if information for a PRP release point number is specified more than once."
+deprecated = ""
+
+[block.packagedata.cellid]
+type = "integer"
+block_variable = false
+valid = []
+shape = "(ncelldim)"
+tagged = "false"
+in_record = "true"
+layered = false
+time_series = false
+reader = "urword"
+optional = false
+preserve_case = false
+numeric_index = false
+mf6internal = "cellid"
+longname = "cell identifier"
+description = "REPLACE cellid {}"
+deprecated = ""
+
+[block.packagedata.xrpt]
+type = "double precision"
+block_variable = false
+valid = []
+shape = ""
+tagged = "false"
+in_record = "true"
+layered = false
+time_series = false
+reader = "urword"
+optional = false
+preserve_case = false
+numeric_index = false
+mf6internal = "xrpt"
+longname = "x coordinate of release point"
+description = "real value that defines the x coordinate of the release point in model coordinates.  The (x, y, z) location specified for the release point must lie within the cell that is identified by the specified cellid."
+deprecated = ""
+
+[block.packagedata.yrpt]
+type = "double precision"
+block_variable = false
+valid = []
+shape = ""
+tagged = "false"
+in_record = "true"
+layered = false
+time_series = false
+reader = "urword"
+optional = false
+preserve_case = false
+numeric_index = false
+mf6internal = "yrpt"
+longname = "y coordinate of release point"
+description = "real value that defines the y coordinate of the release point in model coordinates.  The (x, y, z) location specified for the release point must lie within the cell that is identified by the specified cellid."
+deprecated = ""
+
+[block.packagedata.zrpt]
+type = "double precision"
+block_variable = false
+valid = []
+shape = ""
+tagged = "false"
+in_record = "true"
+layered = false
+time_series = false
+reader = "urword"
+optional = false
+preserve_case = false
+numeric_index = false
+mf6internal = "zrpt"
+longname = "z coordinate of release point"
+description = "real value that defines the z coordinate of the release point in model coordinates or, if the LOCAL\\_Z option is active, in local cell coordinates.  The (x, y, z) location specified for the release point must lie within the cell that is identified by the specified cellid."
+deprecated = ""
+
+[block.packagedata.boundname]
+type = "string"
+block_variable = false
+valid = []
+shape = ""
+tagged = "false"
+in_record = "true"
+layered = false
+time_series = false
+reader = "urword"
+optional = "true"
+preserve_case = false
+numeric_index = false
+mf6internal = "boundname"
+longname = "release point name"
+description = "name of the particle release point. BOUNDNAME is an ASCII character variable that can contain as many as 40 characters. If BOUNDNAME contains spaces in it, then the entire name must be enclosed within single quotes."
+deprecated = ""
+
+[block.period.perioddata]
+type = "recarray releasesetting"
+block_variable = false
+valid = []
+shape = ""
+tagged = true
+in_record = false
+layered = false
+time_series = false
+reader = "urword"
+optional = false
+preserve_case = false
+numeric_index = false
+mf6internal = "perioddata"
+longname = ""
+description = ""
+deprecated = ""
+
+[block.period.releasesetting]
+type = "keystring all first frequency steps fraction"
+block_variable = false
+valid = []
+shape = ""
+tagged = "false"
+in_record = "true"
+layered = false
+time_series = false
+reader = "urword"
+optional = false
+preserve_case = false
+numeric_index = false
+mf6internal = "releasesetting"
+longname = ""
+description = "specifies when to release particles within the stress period.  Overrides package-level RELEASETIME option, which applies to all stress periods. By default, RELEASESETTING configures particles for release at the beginning of the specified time steps. For time-offset releases, provide a FRACTION value."
+deprecated = ""
+
+[block.period.all]
+type = "keyword"
+block_variable = false
+valid = []
+shape = ""
+tagged = true
+in_record = "true"
+layered = false
+time_series = false
+reader = "urword"
+optional = false
+preserve_case = false
+numeric_index = false
+mf6internal = "all"
+longname = ""
+description = "keyword to indicate release of particles at the start of all time steps in the period."
+deprecated = ""
+
+[block.period.first]
+type = "keyword"
+block_variable = false
+valid = []
+shape = ""
+tagged = true
+in_record = "true"
+layered = false
+time_series = false
+reader = "urword"
+optional = false
+preserve_case = false
+numeric_index = false
+mf6internal = "first"
+longname = ""
+description = "keyword to indicate release of particles at the start of the first time step in the period. This keyword may be used in conjunction with other keywords to release particles at the start of multiple time steps."
+deprecated = ""
+
+[block.period.frequency]
+type = "integer"
+block_variable = false
+valid = []
+shape = ""
+tagged = "true"
+in_record = "true"
+layered = false
+time_series = false
+reader = "urword"
+optional = false
+preserve_case = false
+numeric_index = false
+mf6internal = "frequency"
+longname = ""
+description = "release particles at the specified time step frequency. This keyword may be used in conjunction with other keywords to release particles at the start of multiple time steps."
+deprecated = ""
+
+[block.period.steps]
+type = "integer"
+block_variable = false
+valid = []
+shape = "(<nstp)"
+tagged = "true"
+in_record = "true"
+layered = false
+time_series = false
+reader = "urword"
+optional = false
+preserve_case = false
+numeric_index = false
+mf6internal = "steps"
+longname = ""
+description = "release particles at the start of each step specified in STEPS. This keyword may be used in conjunction with other keywords to release particles at the start of multiple time steps."
+deprecated = ""
+
+[block.period.fraction]
+type = "double precision"
+block_variable = false
+valid = []
+shape = "(<nstp)"
+tagged = "true"
+in_record = "true"
+layered = false
+time_series = false
+reader = "urword"
+optional = "true"
+preserve_case = false
+numeric_index = false
+mf6internal = "fraction"
+longname = ""
+description = "release particles after the specified fraction of the time step has elapsed. If FRACTION is not set, particles are released at the start of the specified time step(s). FRACTION must be a single value when used with ALL, FIRST, or FREQUENCY. When used with STEPS, FRACTION may be a single value or an array of the same length as STEPS. If a single FRACTION value is provided with STEPS, the fraction applies to all steps."
+deprecated = ""
diff --git a/flopy4/dfn.py b/flopy4/dfn.py
new file mode 100644
index 0000000..b64c75f
--- /dev/null
+++ b/flopy4/dfn.py
@@ -0,0 +1,95 @@
+from pathlib import Path
+
+import toml
+
+
+class Dfn:
+    def __init__(self, component, subcomponent, dfns, *args, **kwargs):
+        self._component = component
+        self._subcomponent = subcomponent
+        self._dfn = dfns
+        self._blocks = dfns["block"]
+
+    def __getitem__(self, key):
+        return self._blocks[key]
+
+    def __setitem__(self, key, value):
+        self._blocks[key] = value
+
+    def __delitem__(self, key):
+        del self._blocks[key]
+
+    def __iter__(self):
+        return iter(self._blocks)
+
+    def __len__(self):
+        return len(self._blocks)
+
+    @property
+    def component(self):
+        return self._component
+
+    @property
+    def subcomponent(self):
+        return self._subcomponent
+
+    @property
+    def blocknames(self):
+        return self._dfn["blocknames"]
+
+    @property
+    def dfn(self):
+        return self._dfn
+
+    def blocktags(self, blockname) -> list:
+        return list(self._dfn["block"][blockname])
+
+    def block(self, blockname) -> dict:
+        return self._dfn["block"][blockname]
+
+    def param(self, blockname, tagname) -> dict:
+        return self._dfn["block"][blockname][tagname]
+
+    @classmethod
+    def load(cls, f, metadata=None):
+        p = Path(f)
+
+        if not p.exists():
+            raise ValueError("Invalid DFN path")
+
+        component, subcomponent = p.stem.split("-")
+        data = toml.load(f)
+
+        return cls(component, subcomponent, data, **metadata)
+
+
+class DfnSet:
+    def __init__(self, *args, **kwargs):
+        self._dfns = dict()
+
+    def __getitem__(self, key):
+        return self._dfns[key]
+
+    def __setitem__(self, key, value):
+        self._dfns[key] = value
+
+    def __delitem__(self, key):
+        del self._dfns[key]
+
+    def __iter__(self):
+        return iter(self._dfns)
+
+    def __len__(self):
+        return len(self._dfns)
+
+    def add(self, key, dfn):
+        if key in self._dfns:
+            raise ValueError("DFN exists in container")
+
+        self._dfns[key] = dfn
+
+    def get(self, key):
+        if key not in self._dfns:
+            raise ValueError("DFN does not exist in container")
+
+        return self._dfns[key]
diff --git a/pyproject.toml b/pyproject.toml
index dd5abf4..519f84a 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -72,6 +72,7 @@ include = ["flopy4", "flopy4.*"]
 
 [tool.setuptools.package-data]
 "flopy4.dfns" = ["dfns/*.dfn"]
+"flopy4.toml" = ["dfns/toml/*.toml"]
 
 [tool.ruff]
 line-length = 79
@@ -96,4 +97,4 @@ select = [
 ignore = [
     "F821", # undefined name TODO FIXME
     "E722"  # do not use bare `except`
-]
\ No newline at end of file
+]
diff --git a/test/test_dfn.py b/test/test_dfn.py
new file mode 100644
index 0000000..29967c3
--- /dev/null
+++ b/test/test_dfn.py
@@ -0,0 +1,95 @@
+from pathlib import Path
+
+from flopy4.dfn import Dfn, DfnSet
+
+
+class TestDfn(Dfn):
+    __test__ = False  # tell pytest not to collect
+
+
+def test_dfn_load(tmp_path):
+    name = "prt-prp"
+
+    f = Path(f"../dfn/toml/{name}.toml")
+    dfn = Dfn.load(f.absolute(), {})
+
+    assert dfn.component == "prt"
+    assert dfn.subcomponent == "prp"
+    assert type(dfn.dfn) is dict
+    assert len(dfn) == 4
+    assert dfn.blocknames == ["options", "dimensions", "packagedata", "period"]
+
+    for b in dfn.blocknames:
+        block_d = dfn[b]
+        assert type(block_d) is dict
+
+    assert list(dfn.blocktags("options")) == [
+        "boundnames",
+        "print_input",
+        "dev_exit_solve_method",
+        "exit_solve_tolerance",
+        "local_z",
+        "extend_tracking",
+        "track_filerecord",
+        "track",
+        "fileout",
+        "trackfile",
+        "trackcsv_filerecord",
+        "trackcsv",
+        "trackcsvfile",
+        "stoptime",
+        "stoptraveltime",
+        "stop_at_weak_sink",
+        "istopzone",
+        "drape",
+        "release_timesrecord",
+        "release_times",
+        "times",
+        "release_timesfilerecord",
+        "release_timesfile",
+        "timesfile",
+        "dev_forceternary",
+    ]
+
+    assert dfn.param("options", "drape") == {
+        "block_variable": False,
+        "deprecated": "",
+        "description": "is a text keyword to indicate that if a \
+particle's release "
+        "point is in a cell that happens to be inactive at release "
+        "time, the particle is to be moved to the topmost active "
+        "cell below it, if any. By default, a particle is not "
+        "released into the simulation if its release point's cell "
+        "is inactive at release time.",
+        "in_record": False,
+        "layered": False,
+        "longname": "drape",
+        "mf6internal": "drape",
+        "numeric_index": False,
+        "optional": "true",
+        "preserve_case": False,
+        "reader": "urword",
+        "shape": "",
+        "tagged": True,
+        "time_series": False,
+        "type": "keyword",
+        "valid": [],
+    }
+
+
+def test_dfn_container(tmp_path):
+    key = "prt-prp"
+    f = Path(f"../dfn/toml/{key}.toml")
+    dfn = Dfn.load(f.absolute(), {})
+
+    dfns = DfnSet()
+    assert len(dfns) == 0
+    dfns.add(key, dfn)
+    assert len(dfns) == 1
+
+    d = dfns[key]
+    assert type(d) is Dfn
+    assert d is dfn
+    assert d.component == "prt"
+    assert d.subcomponent == "prp"
+    assert dfns.get(key) is d

From 0069b03d6978fca985d4ed664ad6d788b33a3b8d Mon Sep 17 00:00:00 2001
From: mjreno <mjreno@IGSAAA071L01144.gs.doi.net>
Date: Thu, 18 Jul 2024 17:00:03 -0400
Subject: [PATCH 2/4] add toml to project dependencies

---
 pyproject.toml | 1 +
 1 file changed, 1 insertion(+)

diff --git a/pyproject.toml b/pyproject.toml
index 519f84a..9a7b443 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -40,6 +40,7 @@ dependencies = [
     "pandas>=2.0.0",
     "flopy>=3.7.0",
     "Jinja2>=3.0",
+    "toml>=0.10",
 ]
 dynamic = ["version"]
 

From 39dfe918a271dc30db03c2db376e65d7450cf55d Mon Sep 17 00:00:00 2001
From: mjreno <mjreno@IGSAAA071L01144.gs.doi.net>
Date: Thu, 18 Jul 2024 17:23:20 -0400
Subject: [PATCH 3/4] attempt to fix test path

---
 test/test_dfn.py | 9 ++++++---
 1 file changed, 6 insertions(+), 3 deletions(-)

diff --git a/test/test_dfn.py b/test/test_dfn.py
index 29967c3..11b7508 100644
--- a/test/test_dfn.py
+++ b/test/test_dfn.py
@@ -2,15 +2,17 @@
 
 from flopy4.dfn import Dfn, DfnSet
 
+PROJ_ROOT_PATH = Path(__file__).parents[1]
+
 
 class TestDfn(Dfn):
     __test__ = False  # tell pytest not to collect
 
 
 def test_dfn_load(tmp_path):
-    name = "prt-prp"
+    key = "prt-prp"
 
-    f = Path(f"../dfn/toml/{name}.toml")
+    f = Path(PROJ_ROOT_PATH / "dfn" / "toml" / f"{key}.toml")
     dfn = Dfn.load(f.absolute(), {})
 
     assert dfn.component == "prt"
@@ -79,7 +81,8 @@ def test_dfn_load(tmp_path):
 
 def test_dfn_container(tmp_path):
     key = "prt-prp"
-    f = Path(f"../dfn/toml/{key}.toml")
+
+    f = Path(PROJ_ROOT_PATH / "dfn" / "toml" / f"{key}.toml")
     dfn = Dfn.load(f.absolute(), {})
 
     dfns = DfnSet()

From a0780880069c054b8fb3e5d66f0668dc1b968707 Mon Sep 17 00:00:00 2001
From: mjreno <mjreno@IGSAAA071L01144.gs.doi.net>
Date: Fri, 19 Jul 2024 10:41:21 -0400
Subject: [PATCH 4/4] cleanup

---
 flopy4/dfn.py | 22 ++++++++++++++--------
 1 file changed, 14 insertions(+), 8 deletions(-)

diff --git a/flopy4/dfn.py b/flopy4/dfn.py
index b64c75f..80182d5 100644
--- a/flopy4/dfn.py
+++ b/flopy4/dfn.py
@@ -4,26 +4,25 @@
 
 
 class Dfn:
-    def __init__(self, component, subcomponent, dfns, *args, **kwargs):
+    def __init__(self, component, subcomponent, dfn, *args, **kwargs):
         self._component = component
         self._subcomponent = subcomponent
-        self._dfn = dfns
-        self._blocks = dfns["block"]
+        self._dfn = dfn
 
     def __getitem__(self, key):
-        return self._blocks[key]
+        return self._dfn["block"][key]
 
     def __setitem__(self, key, value):
-        self._blocks[key] = value
+        self._dfn["block"][key] = value
 
     def __delitem__(self, key):
-        del self._blocks[key]
+        del self._dfn["block"][key]
 
     def __iter__(self):
-        return iter(self._blocks)
+        return iter(self._dfn["block"])
 
     def __len__(self):
-        return len(self._blocks)
+        return len(self._dfn["block"])
 
     @property
     def component(self):
@@ -93,3 +92,10 @@ def get(self, key):
             raise ValueError("DFN does not exist in container")
 
         return self._dfns[key]
+
+    #def get(self, component, subcomponent):
+    #    key = f"{component.lower()}-{subcomponent.lower()}"
+    #    if key not in self._dfns:
+    #        raise ValueError("DFN does not exist in container")
+    #
+    #    return self._dfns[key]