Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Replace hand-written modelsmodule by a code generation framework #2835

Merged
merged 28 commits into from
Jul 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
20895c6
Add CMake option `-Dminimal-model-set` to reduce compile cycle times
jougs Apr 19, 2023
e20f311
Fix formatting
jougs Apr 20, 2023
6655eb9
Remove superfluous specification of namespace
jougs Jun 21, 2023
2b1a5c3
Make names of secondary connection types consistent with primary ones
jougs Jun 21, 2023
8ecc469
Replace hand-written modelsmodule by a code generation framework
jougs Jun 21, 2023
1a2f5ab
Shifting whitespace
jougs Jun 21, 2023
8600277
Merge branch 'wip' of github.com:jougs/nest-simulator into wip
jougs Jun 21, 2023
83cc873
Remove outdated documentation block
jougs Jun 21, 2023
3b3a145
Remove outdated preprocessor flag
jougs Jun 21, 2023
88e40b2
Remove call to removed function
jougs Jun 21, 2023
3d10da4
Update docstring for -Dwith-models
jougs Jun 21, 2023
3a6f2c0
Fix formatting (F541 f-string is missing placeholders)
jougs Jun 21, 2023
e2980a1
Use "normal" Python to run script
jougs Jun 22, 2023
3dd721c
Also support models derived from other models (e.g. voltmeter)
jougs Jun 22, 2023
0dc88fd
Rename file to be consistent with model defined within it
jougs Jun 22, 2023
0a84bb0
Don't use bare except
jougs Jun 22, 2023
99a953d
Merge branch 'master' of github.com:nest/nest-simulator into wip
jougs Jun 22, 2023
0d50fbc
Fix copyright header, include, and modelset spec after renaming file
jougs Jun 22, 2023
82381f2
Fix error reporting from generation script
jougs Jun 22, 2023
797d1f2
Fix cutting of string to remove trailing semicolon
jougs Jun 22, 2023
ae884a6
Cleanup and change of python executable
jougs Jun 22, 2023
e5c3b89
Adding newline to EOF
jougs Jun 26, 2023
cc855f8
Fix typo
jougs Jun 28, 2023
767a1a9
Merge branch 'master' of github.com:nest/nest-simulator into wip
jougs Jul 10, 2023
ef1cf22
Fix table formatting and add reference
jougs Jul 10, 2023
d2823dc
Update and extend model definition and add link to compile options
jougs Jul 10, 2023
b2ba3fd
Extend modelset documentation
jougs Jul 10, 2023
03282f1
Add informative headers to modelset files
jougs Jul 10, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ set( with-ltdl ON CACHE STRING "Build with ltdl library [default=ON]. To set a s
set( with-gsl ON CACHE STRING "Build with the GSL library [default=ON]. To set a specific library, give the install path." )

# NEST properties
set( with-modelset "full" CACHE STRING "The modelset to include. Sample configurations are in the modelsets directory. This option is mutually exclusive with -Dwith-models. [default=full]." )
set( with-models OFF CACHE STRING "The models to include as a semicolon-separated list of model headers (without the .h extension). This option is mutually exclusive with -Dwith-modelset. [default=OFF]." )
set( tics_per_ms "1000.0" CACHE STRING "Specify elementary unit of time [default=1000 tics per ms]." )
set( tics_per_step "100" CACHE STRING "Specify resolution [default=100 tics per step]." )
set( external-modules OFF CACHE STRING "External NEST modules to be linked in, separated by ';', [default=OFF]." )
Expand Down Expand Up @@ -154,19 +156,19 @@ nest_process_target_bits_split()
nest_process_userdoc()
nest_process_devdoc()

nest_process_models()

# These two function calls must come last, as to prevent unwanted interactions of the newly set flags
# with detection/compilation operations carried out in earlier functions. The optimize/debug flags set
# using these functions should only apply to the compilation of NEST.
# using these functions should only apply to the compilation of NEST, not to that of test programs
# generated by CMake when it tries to detect compiler options or such.
nest_process_with_optimize()
nest_process_with_debug()

nest_get_color_flags()
set( CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${NEST_C_COLOR_FLAGS}" )
set( CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${NEST_CXX_COLOR_FLAGS}" )

# requires HAVE_LIBNEUROSIM
nest_default_modules()

nest_write_static_module_header( "${PROJECT_BINARY_DIR}/nest/static_modules.h" )

# check additionals
Expand Down
340 changes: 340 additions & 0 deletions build_support/generate_modelsmodule.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,340 @@
# -*- coding: utf-8 -*-
#
# generate_modelsmodule.py
#
# This file is part of NEST.
#
# Copyright (C) 2004 The NEST Initiative
#
# NEST is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
#
# NEST is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with NEST. If not, see <http://www.gnu.org/licenses/>.

"""Script to generate the modelsmodule implementation file.

This script is called during the run of CMake and generates the file
models/modelsmodule.cpp as well as the list of source files to be
compiled by CMake.
"""

import os
import sys
import argparse

from pathlib import Path
from textwrap import dedent


def parse_commandline():
"""Parse the commandline arguments and put them into variables.

There are three arguments to this script that can be given either as
positional arguments or by their name.

1. srcdir: the path to the top-level NEST source directory
2. blddir: the path to the NEST build directory (-DCMAKE_INSTALL_PREFIX)
3. models: the semicolon-separated list of models to be built in

This function does not return anything, but instead it checks the
commandline arguments and makes them available as global variables
of the script. ``srcdir`` and ``blddir`` are set as they were
given. The model list is split and commented models (i.e. ones
that start with '#') are filtered out. The list is then made
available under the name model_names.
"""

global srcdir, blddir, model_names

description = "Generate the implementation and header files for modelsmodule."
parser = argparse.ArgumentParser(description=description)
parser.add_argument("srcdir", type=str, help="the source directory of NEST")
parser.add_argument("blddir", type=str, help="the build directory of NEST")
parser.add_argument("models", type=str, help="the models to build into NEST")
args = parser.parse_args()

srcdir = args.srcdir
blddir = args.blddir

model_names = [model_file.strip() for model_file in args.models.split(";")]
model_names = [model for model in model_names if model and not model.startswith("#")]


def get_models_from_file(model_file):
"""Extract model information from a given model file.

This function applies a series of simple heuristics to find the
preprocessor guards and the list of models in the file. Guards are
expected to be in the form "#ifdef HAVE_<LIB>" with one guard per
line. For the models, a list of unique pattern is used to infer
the correct model type from the line in the file.

The majority of neuron, device, and connection models are classes
derived from a specific base class (like Node, ArchivingNode, or
Connection) or from another model. The latter can only be detected
if the base model has the same name as the file.

The rate and binary neurons are typedefs for specialized template
classes and multiple of such typedefs may be present in a file.

Parameters
----------
model_file: str
The base file name (i.e. without extension) of the model file
to get the information from.

Returns
-------
tuple with two components:
0: HAVE_* preprocessor guards required for the models in the file
1: a zip of model types and model names found in the file and
that need registering

"""

model_patterns = {
"neuron": "public ArchivingNode",
"stimulator": "public StimulationDevice",
"recorder": "public RecordingDevice",
"devicelike": "public DeviceNode",
"connection": "public Connection",
"node": "public Node",
"clopath": "public ClopathArchivingNode",
"urbanczik": "public UrbanczikArchivingNode",
"binary": "typedef binary_neuron",
"rate": "typedef rate_",
}

fname = Path(srcdir) / "models" / f"{model_file}.h"
if not os.path.exists(fname):
print(f"ERROR: Model with name {model_file}.h does not exist", file=sys.stderr)
sys.exit(128)

guards = []
names = []
types = []
with open(fname, "r") as file:
for line in file:
if line.startswith("#ifdef HAVE_"):
guards.append(line.strip().split()[1])
if line.startswith(f"class {model_file} : "):
for mtype, pattern in model_patterns.items():
if pattern in line:
names.append(model_file)
types.append(mtype)
if line.startswith("class") and line.strip().endswith(f" : public {model_file}"):
names.append(line.split(" ", 2)[1])
# try to infer the type of the derived model from the base model,
# assuming that that was defined earlier in the file
try:
types.append(types[names.index(model_file)])
except (ValueError, KeyError) as e:
types.append("node")
if line.startswith("typedef "):
for mtype, pattern in model_patterns.items():
if pattern in line:
names.append(line.rsplit(" ", 1)[-1].strip()[:-1])
types.append(mtype)

return tuple(guards), zip(types, names)


def get_include_and_model_data():
"""Create data dictionaries for include files and models.

This function creates two nested dictionaries.

The first (`includes`) contains the a mapping from model_type ->
guards -> model_includes and is used in the code generation
function to print all include lines. This basically corresponds to
the list handed to the script as the `models` command line
argument, but is enriched by model type information and the
preprocessor guards needed for the individual include files.

The second (`models`) is a mapping from model_type -> guards ->
model_names and is used to generate the actual model registration
lines. model_names here is a list of models that is potentially
larger than the ones coming in throught the `models` command line
argument, as each file could contain multiple model definitions.

This function does not return anything, but instead sets the
global variables `includes` and `models` to be used by the code
generation function.

"""

global includes, models

includes = {}
models = {}

for model_file in model_names:
guards, model_types_names = get_models_from_file(model_file)
for tp, nm in model_types_names:
# Assemble a nested dictionary for the includes:
fname = model_file + ".h"
if tp in includes:
if guards in includes[tp]:
includes[tp][guards].add(fname)
else:
includes[tp][guards] = set([fname])
else:
includes[tp] = {guards: set([fname])}

if (Path(srcdir) / "models" / f"{model_file}_impl.h").is_file():
includes[tp][guards].add(f"{model_file}_impl.h")

# Assemble a nested dictionary for the models:
if tp in models:
if guards in models[tp]:
models[tp][guards].append(nm)
else:
models[tp][guards] = [nm]
else:
models[tp] = {guards: [nm]}


def start_guard(guards):
"""Print an #ifdef line with preprocessor guards if needed."""

if guards:
guard_str = " && ".join([f"defined( {guard} )" for guard in guards])
return f"#if {guard_str}\n"
else:
return ""


def end_guard(guards):
"""Print an #endif line for the preprocessor guards if needed."""
return "#endif\n" if guards else ""


def generate_modelsmodule():
"""Write the modelsmodule implementation out to file.

This is a very straightforward function that prints several blocks
of C++ code to the file modelsmodule.cpp in the `blddir` handed as
a commandline argument to the script. The blocks in particular are

1. the copyright header.
2. a list of generic NEST includes
3. the list of includes for the models to build into NEST
4. some boilerplate function implementations needed to fulfill the
Module interface
5. the list of model registration lines for the models to build
into NEST

The code is enriched by structured C++ comments as to make
debugging of the code generation process easier in case of errors.

"""

fname = Path(srcdir) / "doc" / "copyright_header.cpp"
with open(fname, "r") as file:
copyright_header = file.read()

fname = "modelsmodule.cpp"
modeldir = Path(blddir) / "models"
modeldir.mkdir(parents=True, exist_ok=True)
with open(modeldir / fname, "w") as file:
file.write(copyright_header.replace("{{file_name}}", fname))
file.write(
dedent(
"""
#include "modelsmodule.h"
jougs marked this conversation as resolved.
Show resolved Hide resolved

// Generated includes
#include "config.h"

// Includes from nestkernel
#include "common_synapse_properties.h"
#include "connector_model_impl.h"
#include "genericmodel.h"
#include "genericmodel_impl.h"
#include "kernel_manager.h"
#include "model_manager_impl.h"
#include "target_identifier.h"
"""
)
)

for model_type, guards_fnames in includes.items():
file.write(f"\n// {model_type.capitalize()} models\n")
for guards, fnames in guards_fnames.items():
file.write(start_guard(guards))
for fname in fnames:
file.write(f'#include "{fname}"\n')
file.write(end_guard(guards))

file.write(
dedent(
"""
nest::ModelsModule::ModelsModule()
{
}

nest::ModelsModule::~ModelsModule()
{
}

const std::string
nest::ModelsModule::name() const
{
return std::string( "NEST standard models module" );
}

void
nest::ModelsModule::init( SLIInterpreter* )
{"""
)
)

conn_reg = ' register_connection_model< {model} >( "{model}" );\n'
node_reg = ' kernel().model_manager.register_node_model< {model} >( "{model}" );\n'

for model_type, guards_mnames in models.items():
file.write(f"\n // {model_type.capitalize()} models\n")
for guards, mnames in guards_mnames.items():
file.write(start_guard(guards))
for mname in mnames:
if model_type == "connection":
file.write(conn_reg.format(model=mname))
else:
file.write(node_reg.format(model=mname))
file.write(end_guard(guards))

file.write("}")


def print_model_sources():
"""Hand back the list of model source files to CMake.

In addition to the header file names handed to the script in the
form of the `models` commandline argument, this function searches
for corresponding implementation files with the extensions `.cpp`
and `_impl.h`. The list of models is printed as a CMake list,
i.e. as a semicolon separated string.

"""

model_sources = []
source_files = os.listdir(Path(srcdir) / "models")
for model_name in model_names:
source_candidates = [model_name + suffix for suffix in (".cpp", ".h", "_impl.h")]
model_sources.extend([f for f in source_files if f in source_candidates])
print(";".join(model_sources), end="")


if __name__ == "__main__":
parse_commandline()
get_include_and_model_data()
generate_modelsmodule()
print_model_sources()
9 changes: 8 additions & 1 deletion cmake/ConfigureSummary.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ function( NEST_PRINT_CONFIG_SUMMARY )
message( "--------------------------------------------------------------------------------" )
message( "NEST Configuration Summary" )
message( "--------------------------------------------------------------------------------" )

message( "" )
if ( CMAKE_BUILD_TYPE )
message( "Build type : ${CMAKE_BUILD_TYPE}" )
Expand All @@ -36,7 +37,13 @@ function( NEST_PRINT_CONFIG_SUMMARY )
message( "Build dynamic : ${BUILD_SHARED_LIBS}" )

message( "" )
message( "Built-in modules : ${SLI_MODULES}" )
if ( with-models )
message( "Built-in models : ${BUILTIN_MODELS}" )
else ()
message( "Built-in modelset : ${with-modelset}" )
endif ()

message( "" )
if ( external-modules )
message( "User modules : ${external-modules}" )
foreach ( mod ${external-modules} )
Expand Down
Loading
Loading