Java Classes
Warning
This page of our documentation is not actively updated, as we work on migrating code documentation to docstrings. Most of the information herein remains revelant, but please refer to the codebase for the most up to date structure.
ModelWrapper Class
The ModelWrapper
class in Java takes inputs of the ASCENT_PROJECT_PATH
(env.json
, JSON Configuration Files) and a list of Run paths. ModelWrapper
contains a COMSOL
“model” object, model source directory (String), model destination
directory (String), an "IdentifierManager"
(Java Utility Classes), and HashMaps
with key-value
pairs linking COMSOL domains to unions of domains. ModelWrapper
has
accessor methods getModel()
for retrieving the model object, getRoot()
for retrieving the project path’s root String, and getDest()
for
retrieving the default saving destination String. ModelWrapper
also has
mutator methods for changing an instance’s root String (setRoot()
) and
default saving destination String (setDest()
).
ModelWrapper’s
main()
method starts an instance of COMSOL and loads
Run, Sample, Model, and Sim configurations as
JSON Objects into memory. We developed Java class JSONio
(Java Utility Classes) for reading and
writing JSON Objects to file.
Since each Run contains a list of Model and Sim
configurations for a single Sample (note: n_sims/
are created for
all combinations of Model and Sim for the Sample in a
Run), ModelWrapper
iterates over Model configurations (e.g.,
different cuff electrodes or material assignments) to define the FEM
geometry, mesh, assign boundary conditions and physics, and solve. The
resulting FEM potentials are obtained for 1 mA applied to one of the
electrode contacts while the electric potential on the other contacts is
floating (i.e., condition of continuity); this is repeated for each
contact to define the “bases” of the solution space [Pelot et al., 2018]. For each
Sim, the program then creates a superposition of the “bases” for
extracellular potentials at the coordinates defined in fibersets/
and
ss_coords/
(i.e., the coordinates along the length of the nerve used to
“super-sample” potentials for later creating potentials/ without the
need for COMSOL). We wrote the code such that the program will
continue with creating potentials/, ss_bases/
(i.e., the potentials
along the length of the nerve corresponding 1:1 to the coordinates saved
in ss_coords/
, which are added together according to the contact
weighting defined by "active_srcs"
in Sim to create potentials/ for
specific fiber models), and NEURON simulations for any remaining
Model indices even if the processes for a single Model
fails. For each Model, the program appends a Boolean to
"models_exit_status"
in Run (true if successful, false if not
successful).
ModelWrapper.addNerve()
The addNerve()
method adds the nerve components to the COMSOL “model”
object using Java. If the "NerveMode"
in Sample (“nerve”) is
“PRESENT” (Sample Parameters) the program creates a part instance of epineurium using the
createNervePartInstance()
method in Part (src/model/Part.java
). The
addNerve()
method then searches through all directories
in fascicles/
for the sample being modeled, and, for each fascicle,
assigns a path for the inner(s) and outer in a HashMap
. The HashMap
of
fascicle directories is then passed to the createNervePartInstance()
method in Part which adds fascicles to the COMSOL “model” object.
ModelWrapper.extractAllPotentials()
The user is unlikely to interface directly with ModelWrapper’s
extractAllPotentials()
method in Java as it operates behind the scenes.
The method takes input arguments for the project path and a run path.
Using the run path, the method loads Run, and constructs lists of
Model and Sim for which it will call extractPotentials()
for
each fiberset. COMSOL is expecting a (3 ⨉ n)
matrix of coordinates (Double[3][n]), defining the
(x,y,z)-coordinates for each of n points.
The Java COMSOL API methods setInterpolationCoordinates()
and getData()
for a model object are fast compared to the time for a machine to load a
COMSOL “model” object to memory from file. Therefore, the
extractAllPotentials()
method is intentionally configured to minimize
the number of times a “basis” COMSOL “model” object is loaded into
memory. We accomplish this by looping in the following order:
Model, bases, Sims, fibersets (i.e., groups of fibers with
identical geometry/channels, but different (x,y)-locations and/or
longitudinal offsets), then fibers. With this approach, we load each
COMSOL “model” object only once (i.e., *.mph members of bases/). Within
the loop, the extractPotentials()
method constructs the bases
(double[basis index][sim index][fiberset index][fiber index])
for each model (units: Volts). With the bases in memory, the program
constructs the potentials for inputs to NEURON by combining bases by
their contact weights and writes them to file within potentials/ (or
ss_bases/), which mirrors fibersets/ (or ss_coords/) in
contents.
ModelWrapper.addMaterialDefinitions()
The user is unlikely to interface directly with the
addMaterialDefinitions()
method in Java as it operates behind the
scenes. The method takes an input of a list of strings containing the
material functions (i.e., endoneurium, perineurium, epineurium, cuff
“fill”, cuff “insulator”, contact “conductor”, and contact “recess”),
Model, and a COMSOL ModelParamGroup
for containing the material
properties as a COMSOL “Parameters Group” under COMSOL’s “Global
Definitions”. The method then loops over all material functions, creates
a new material if it does not yet exist as "mat<#>"
using Part’s
defineMaterial()
method, and adds the identifier (e.g., “mat1”) to the
IdentifierManager
.
ModelWrapper.addCuffPartMaterialAssignment()
The user is unlikely to interface directly with the
addCuffPartMaterialAssignment()
method in Java as it operates behind the
scenes. The method loads in a cuff part primitive’s labels from its
IdentifierManager. The method loops over the list of material JSON
Objects in the “preset” cuff configuration file. For each material
function in the “preset” cuff configuration file, the method creates a
COMSOL Material Link to assign a previously defined selection in a cuff
part instance to a defined material.
ModelWrapper.addCuffPartMaterialAssignments()
The user is unlikely to interface directly with the
addCuffMaterialAssignments()
method in Java as it operates behind the
scenes. The method loops through all part instances in the cuff
configuration file, which is linked to Model by a string of its
file name under the “preset” key, and calls the
addCuffMaterialAssignment()
method for each part instance. As described
in addCuffPartMaterialAssignment()
above, a material is connected to the
selection within the part primitive by its label. In COMSOL, material
links appear in the “Materials” node. COMSOL assigns the material links
in the order of the part instances defined in the “preset” cuff
configuration file, which is important since material links overwrite
previous domain assignments. For this reason, it is important to list
part instances in “preset” cuff files in a nested order (i.e., the
outermost domains first, knowing that domains nested in space within
them will overwrite earlier domain assignments).
Making geometries in COMSOL (Part class)
Part.createEnvironmentPartPrimitive()
The createEnvironmentPartPrimitive()
method in Java
(src/model/Part.java
) creates a “part” within the “Geometry Parts” node
of the COMSOL “model” object to generalize the cylindrical medium
surrounding the nerve and electrode. Programmatically selecting domains
and surfaces in COMSOL requires that geometry operations be contributed
to “selections” (csel<#>
). In this simple example of a part
primitive, the im.labels String[]
contains the string “MEDIUM” which
is used to label the COMSOL selection (csel<#>
) for the medium domain
by association with an IdentifierManager
(Java Utility Classes). When the geometry of the
primitive is built, the resulting medium domain’s csel<#>
can be
accessed instead with the key “MEDIUM” in the IdentifierManager
, thereby
improving readability and accessibility when materials and boundary
conditions are assigned in the createEnvironmentPartInstance()
method.
Furthermore, if the operations of a part primitive are modified, the
indexing of the csel<#>
labels are automatically handled.
Part.createCuffPartPrimitive()
The createCuffPartPrimitive()
method in Java (src/model/Part.java
) is
analogous to createEnvironmentPartPrimitive()
, except that it contains
the operations required to define cuff part geometries, which are
generally more complex. Examples of cuff part primitives include
standard geometries for contact conductors (e.g., Ribbon Contact
Primitive, Wire Contact Primitive, Circle Contact Primitive, and
Rectangular Contact Primitive), cuff insulation (e.g., Tube Cuff), cuff
fill (e.g., saline, mineral oil), and specific interactions of a cuff
insulator and electrode contact (e.g., LivaNova-inspired helical coil)
(ASCENT Part Primitives).
Part Instances
Part instances are a COMSOL Geometry Feature ("pi<#>"
) in the
“Geometry” node based on user-defined input parameters stored in
Model and default parameters for “preset” cuffs. A part instance
is an instantiation of a part primitive previously defined in the COMSOL
“model” object and will take specific form based on its input
parameters.
Part.createEnvironmentPartInstance()
The createEnvironmentPartInstance()
method in Java creates a “part
instance” in COMSOL’s “Geometry” node based on a primitive previously
defined with createEnvironmentPartPrimitive()
. This method just applies
to building the surrounding medium. The method takes inputs, with data
types and examples in parentheses: instanceID
(String: "pi<#>"
),
instanceLabel
(String: “medium”), mediumPrimitiveString
(String: Key for
the medium part stored in the identifierManager
), an instance of
ModelWrapper
, and Model as a JSON Object. Within the “medium” JSON
Object in Model, the parameters required to instantiate the
environment part primitive are defined.
Part.createCuffPartInstance()
The createCuffPartInstance()
method in Java is analogous to
createEnvironmentPartInstance()
, but it is used to instantiate cuff part
geometries. We decided to separate these methods since all products of
createCuffPartInstance()
will be displaced and rotated by the same cuff
shift (x,y,z) and rotation values.
Part.createNervePartInstance()
The createNervePartInstance()
method in Part (src/model/Part.java
)
creates three-dimensional representations of the nerve sample including
its endoneurium, perineurium, and epineurium. The
createNervePartInstance()
method defines domain and surface geometries
and contributes them to COMSOL selections (lists of indices for domains,
surfaces, boundaries, or points), which are necessary to later assign
physics properties.
Fascicles
ASCENT uses CAD sectionwise
files (i.e., ASCII with .txt
extension containing column vectors for x- and y-coordinates) created by
the Python Sample class to define fascicle tissue boundaries in COMSOL.
We provide the "use_ci"
mode in Model (Model Parameters) to model the perineurium
using COMSOL’s contact impedance boundary condition for fascicles with
only one inner (i.e., endoneurium) domain for each outer (i.e.,
perineurium) domain (Perineurium Properties). If "use_ci"
mode is true, the perineurium for all
fascicles with exactly one inner and one outer is represented with a
contact impedance. The pipeline does not support control of using the
contact impedance boundary condition on a fascicle-by-fascicle basis.
The createNervePartInstance()
method in Part (src/model/Part.java
)
performs a directory dive on the output CAD files
samples/<sample_index>/slides/<#>/<#>/sectionwise2d/fascicles/<outer,inners>/
from the Sample class in Python to create a fascicle for
each outer. Depending on the number of corresponding inners for each
outer saved in the file structure and the "use_ci"
mode in Model,
the program either represents the perineurium in COMSOL as a surface
with contact impedance (FascicleCI: Fascicles with one inner per outer
and if "use_ci"
mode is true) or with a three-dimensional meshed domain
(FascicleMesh: Fascicles with multiple inners per outer or if "use_ci"
parameter is false).
Epineurium
The createNervePartInstance()
method in Part (src/model/Part.java
)
contains the operations required to represent epineurium in COMSOL. The
epineurium cross-section is represented one of two ways:
If
deform_ratio
in Sample is set to 1 and"DeformationMode"
is not"NONE"
, the nerve shape matches the"ReshapeNerveMode"
from Sample (e.g.,"CIRCLE"
). (Sample Parameters). An epineurium boundary is then created from this shape.Otherwise, the coordinate data contained in
samples/<sample_index>/slides/<#>/<#>/sectionwise2d/nerve/0/0.txt
is used to create an epineurium boundary.
The epineurium boundary is then extruded into the third dimension. This
is only performed if the "NerveMode"
(i.e., “nerve”) in Sample is
"PRESENT"
and n.tif
is provided (Sample Parameters).
Part.defineMaterial()
The user is unlikely to interface directly with the defineMaterial()
method in Java as it operates behind the scenes to add a new material to
the COMSOL “Materials” node under “Global Definitions”. The method takes
inputs of the material’s identifier in COMSOL (e.g., “mat1”), function
(e.g., cuff “fill”), Model, a library of predefined materials
(e.g., materials.json
), a ModelWrapper instance, and the COMSOL
ModelParamGroup for material conductivities. The defineMaterial()
method
uses materials present in Model’s “conductivities” JSON Object to
assign to each material function in the COMSOL model (e.g., insulator,
conductor, fill, endoneurium, perineurium, epineurium, or medium). The
material value for a function key in Model is either a string for
a pre-defined material in materials.json
, or a JSON Object (containing
unit, label, and value) for a custom material. Assigning material
conductivities to material functions in this manner enables control for
a user to reference a pre-defined material or explicitly link a custom
material to a COMSOL model. In either the case of a predefined material
in materials.json
or custom material in Model, if the material is
anisotropic, the material value is assigned a string “anisotropic” which
tells the program to look for independent "sigma_x"
, "sigma_y"
, and
"sigma_z"
values in the material JSON Object.
Java utility classes
IdentifierManager
In working with highly customized COMSOL FEMs, we found it convenient to
abstract away from the underlying COMSOL indexing to improve code
readability and enable scalability as models increase in geometric
complexity. We developed a class named IdentifierManager
that allows the
user to assign their own String to identify a COMSOL geometry feature
tag (e.g., wp<#>
, cyl<#>
, rev<#>
, dif<#>
) or selection
(i.e., csel<#>
).
We use IdentifierManagers
to name and keep track of identifier labels
within a PartPrimitive
. IdentifierManagers
assigning tags for products
of individual operations to a unique “pseudonym” in
HashMaps
as
a key-value pair. Additionally, we assign resulting selections
("csel<#>"
) to meaningful unique pseudonyms to later assign to
meshes, materials, or boundary conditions.
We keep a running total for tags of IdentifierStates
(i.e., the total
number of uses of an individual COMSOL tag) and HashMap
of
IdentifierPseudonyms
(i.e., a HashMap
containing key (pseudonym) and
value (COMSOL tag, e.g., “wp1”)).
IdentifierManager
has a method next()
which takes inputs of a COMSOL tag
(e.g., wp
, cyl
, rev
, dif
) or selection (i.e., csel
) without its index
and a user’s unique pseudonym String. The method appends the next index
(starting at 1) to the COMSOL tag or selection and puts the COMSOL tag
or selection and associated pseudonym in the IdentifierPseudonyms
HashMap
. The method also updates the IdentifierStates
HashMap
total for
the additional instance of a COMSOL tag or selection.
To later reference a COMSOL tag or selection, IdentifierManager
has a
get()
method which takes the input of the previously assigned pseudonym
key and returns the COMSOL tag or selection value from the
IdentifierPseudonyms
HashMap
.
To accommodate mesh recycling (see ModelSearcher
below), we save a
COMSOL model’s IdentifierManagers
to later access selections for
updating model materials and physics. Therefore, we developed
IdentifierManager
methods toJSONObject()
and fromJSONObject()
which
saves an IdentifierManager
to a JSON file and loads an IdentifierManager
into Java from a JSON Object, respectively.
JSONio
JSONio
is a convenient Java class used for reading and writing JSON
Objects to file. The read()
method takes an input String containing the
file path to read and returns a JSON Object to memory. The write()
method takes an input String containing the file path for the saving
destination and a JSON Object containing the data to write to file.
ModelSearcher
The ModelSearcher
class in Java is used to look for previously created
FEM meshed geometries that can be repurposed. For example, if
Model configurations differ only in their material properties or
boundary conditions and the previous Model’s *.mph file with the
mesh (i.e., mesh.mph
) was saved, then it is redundant to build and mesh
the same model geometry for a new Model configuration. The methods
of the ModelSearcher
class can save enormous amounts of computation time
in parameter sweeps of Model if the mesh can be recycled. The user
is unlikely to interface directly with this method as it operates behind
the scenes, but if the user adds new parameter values to Model,
then the user must also add those values to
config/templates/mesh_dependent_model.json
to indicate whether the
added parameter value needs to match between FEMs to recycle the mesh
(explained further below). Generally, changes in geometry or meshing
parameters need to match, but changes in material properties or boundary
conditions do not, since they do not change the FEM geometry.
Specifically, this class compares Model configurations to
determine if their parameters are compatible to repurpose the geometry
and mesh from a previously generated COMSOL model using the meshMatch()
method. The meshMatch()
method takes the inputs of a reference JSON
(i.e., config/templates/mesh_dependent_model.json
, see Mesh Dependent Model) containing
conditions for compatibility and a JSON Object for each of two
Model configurations to compare. The parameter keys correspond
one-to-one in Model and mesh_dependent_model.json
. However, in
mesh_dependent_model.json
, rather than numerical or categorical values
for each parameter key, the keys’ values are a Boolean indicating if the
values between two Model configurations must be identical to
define a “mesh match”. For two Model configurations to be a match,
all parameters assigned with the Boolean true in
mesh_dependent_model.json
must be identical. Model
configurations that differ only in values for parameters that are
assigned the Boolean false are considered a mesh match and do not
require that the geometry be re-meshed.
In the class’s searchMeshMatch()
method, the program looks through all
Model configurations under a given Sample and applies the
meshMatch()
method. If a Model match is found, searchMeshMatch
returns a Match class, which is analogous to the ModelWrapper
class,
using the path of the matching Model with the fromMeshPath()
method.