Skip to content
Merged
2 changes: 1 addition & 1 deletion .clang-tidy
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,6 @@ Checks: >
google-*
CheckOptions:
- key: readability-function-cognitive-complexity.Threshold
value: '37'
value: '40'
ExtraArgs: ['-std=c++20']
WarningsAsErrors: '*'
113 changes: 81 additions & 32 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@

ROOT, but not the [particle physics one](https://github.com/root-project/root). Project submission for MATH-458: Programming concepts in scientific computing.

## Project structure and dependencies
The project bundles a header-only C++ library (`libROOT`) implementing root-finding algorithms and a CLI application (`root_cli`) to read and parse input, run the algorithms implemented in libROOT, and write the output to a file of specific format.

## Project structure

The project uses the recommended [Canonical Project Structure](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p1204r0.html) for C++ projects.

Expand Down Expand Up @@ -37,40 +39,70 @@ The project uses the recommended [Canonical Project Structure](https://www.open-
└── tests # Tests for the ROOT library
```

### Dependencies
Apart from being divided into a library and a user-facing application/executable, the design on the project
is concretely split into four phases.

### CLI

The CLI application is written (and should be written) within the `main` function. The `main` function further calls the `Reader`, `Solver`, and `Writer` classes (in this order) on the input passed through the CLI application.

### Readers and Parsers

Reading and parsing is handled by the `ReaderBase` and `FunctionParserBase` daughter classes. Adding a new reading method should include writing a new `ReaderBase` daughter class and adding functionality to parse a new type of function should include writing a new `FunctionParserBase` daughter class. The information read is stored by the `ConfigBase` daughter classes (these are data classes to be specific, and can ideally by just `struct`s, but they use some object-oriented features, requiring them to be `class`es). Adding a new stepper type should include adding a new `ConfigBase` daughter class. The `read` method of the `ReaderBase` daughter classes accept a pointer of the type `CLI::App` and return a pointer to an object of the type of one of the daughter classes of `ConfigBase`. The `Reader` classes further implement helper methods for reading and parsing different things, and functions for constructing the `ConfigBase` object itself. Similarly, the `parse` function of the `FunctionParser` classes takes in a `string` and returns a C++ function (parses a specific type of function). The classes also include helper methods for parsing functions, and a method (in `FunctionParserBase`) to infer the type of the function (from the string) and dispatch the appropriate daughter class objects (and methods) to parse the function.

### Solver and Steppers

The solution of the non-linear equation is completely handled by two types of `class`es: `Solver` and `StepperBase`, with her daughter specialized for each method (for now: Newton-Raphson, Bisection, Chords, Fixed Point).
Comment thread
Saransh-cpp marked this conversation as resolved.
Outdated
The `Solver` class is constructed with all the inputs required and taken from the previous reading and configuring steps, and has methods to manage the outer passages involved in the solution, all of which are called inside a unique `solve` method. These steps involve mainly the convergence check, the results saving, and the definition and call of the object specialized in computing the single step of the numerical method itself.
Comment thread
Saransh-cpp marked this conversation as resolved.
Outdated
`Solver` has no daughter classes but could be refactored to be daughter of a `SolverBase` class which does everything which is in common for all the numerical methods (convergence check and while loop); this refactored `SolverNonLinear` would inherit all the methods from the mother class and add arguments for the functions and the boolean to require Aitken's acceleration. This new `SolverNonLinear` could have daughter classes for solving single equations (our current `Solver`) or systems of them, which would differ just in the type of the arguments saved (e.g. derivative/jacobian for Newton-Raphson). This draft idea, which could be substituted by a fully templated version of the `SolverNonLinear` class, comes from the fact that templating is already used to define the different kinds of initial guesses allowed, and it is not possible (in C++) to partially specialize different templates. Another more brute-force idea could be to define all the different arguments as matrices and then use them as 1 by 1 matrices (or vectors) for the single equation case, without creating two daughter classes. All of these ideas would have to be adapted for the `Stepper` classes too.
Comment thread
Saransh-cpp marked this conversation as resolved.
Outdated
A `StepperBase` object is constructed inside the `Solver::solve` method, initially as completely virtual. Then it is converted into a specialized daughter class of it, with all the required arguments to use for the single step computation.
The only method executed by the Steppers is `compute_step` which computes a single step of the numerical method and returns the results for it.
To allow more numerical methods, it is possible to simply define new daughter classes with different `compute_step` algorithms and potentially different arguments to store.
Comment thread
Saransh-cpp marked this conversation as resolved.
Outdated

#### Dependencies for the project
### Writer and Printers

##### Required
The writing part of the project is done again by two major `class`es: `Writer` and `PrinterBase`, with her daughter classes for each output destination available.
All the inputs required to define how and where to write the results are defined in the reading and configuring step.
Comment thread
Saransh-cpp marked this conversation as resolved.
Outdated
What is important to point out is that these classes are not defined as only applicable for our specific project, but can write anything correctly passed (potentially with slight refactoring of the code).
This classes' methods are coded just for the typed version required in out project, but different typed version would be easy to add.
Comment thread
Saransh-cpp marked this conversation as resolved.
Outdated
`Writer` has arguments to store what to write and how, and methods (all of which are called by a unique `write` one) to define, convert and handle a `PrinterBase` object.
`PrinterBase` is then specialized for a certain output destination, all of which have a overriden `write_values` method which prints a given input on a stored output.
To allow different writing destinations, new daughter classes can be defined inheriting from existing ones.
Comment thread
Saransh-cpp marked this conversation as resolved.
Outdated

## Dependencies

### Dependencies for the project

#### Required

The required dependencies are included within the project as git submodules and are pinned to specific
versions for reproducibility.

- `CLI11` (`v2.6.1`): for constructing the CLI interface for the user-facing `root_cli` application.
- `Eigen3` (`v5.0.1`): for matric and vector usage / calculations.

##### Optional
#### Optional

These can be installed by a user and are not installed through the project's build system.

- `gnuplot`: for plotting results

#### Required dependencies for the tests
### Required dependencies for the tests

`gnuplot` must be installed before building the project with `-DTEST=ON`. `GoogleTest` is installed automatically if the project is built with `-DTEST=ON`.

- `GoogleTest` (`v1.17.0`): for all tests.
- `gnuplot`: for testing `gnuplot` related code.

#### Dependencies for the documentation
### Dependencies for the documentation

These can be installed by a user and are not installed through the project's build system.

##### Required
#### Required

- `doxygen`: for generating the documentation.

##### Optional
#### Optional

- `graphviz`: for generating hierarchy and flow diagrams in the documentation.

Expand Down Expand Up @@ -142,7 +174,8 @@ root_cli <arguments>
In order to print out more information about the arguments and the subcommands:

```
root_cli <arguments>
root_cli --help
root_cli <subcommand> --help
```

Every additional needed function must be added together with the function to find the root of.
Expand All @@ -154,60 +187,66 @@ Here's a list of examples of possible execution syntax:
root_cli --wcli cli --function "x^2-4" newton --initial 1.0 --derivative "2*x"
```

- DAT input file called input.dat with first row not being header and " " separating different values, .dat file output called output.dat, Bisection method to find the root of x^3-1, with initial interval [-2,2], verbose output (given tolerance and maximum iterations):
- DAT input file called input.dat, DAT output file called output.dat, Bisection method to find the root of x^3-1, with initial interval [-2,2], and verbose output (given tolerance and maximum iterations):

```
root_cli --verbose --wdat output dat input
root_cli --verbose --wdat output dat --file input.dat
```

where input.dat is:

```
function = x^3-1
method = bisection
initial = -1
interval_a = -2
interval_b = 2
tolerance = 1e-5
max-iterations = 100
derivative = 2*x
```

- CSV input file called input.csv with first row which is a header and "," separating different values, .csv file ouput
called output.csv, Fixed Point Method to find the root of cos(x), with
initial guess 0.5, fixed point function cos(x):
- CSV input file called input.csv with first row which is a header and "," separating different values, CSV output file called output.csv, Fixed Point Method to find the root of x^2-x, with initial guess 0.5, fixed point function x^2, and verbose output (given tolerance and maximum iterations)::

```
root_cli --wcsv output --ocsvsep , csv input --sep , --header
root_cli --verbose --wcsv output --ocsvsep , csv --file input.csv --sep ,
```

where input.csv is:

```
function,method,initial,tolerance,max_iterations,g-function
'cos(x)',fixed_point,0.5,1e-5,100,'cos(x)'
x^2-x,fixed_point,0.5,1e-65,100,x^2
```

- CLI input, .dat output file called output.dat and moreover a GNU Plot is created from it as output.png. Chords method to solve
the equation x^3-8 starting from the two initial points 1 and 3:
- Same as above but with aitken acceleration (will converge faster):

```
root_cli --wdat output --wgnuplot output cli --function x^3-8 chords --x0 --x1 3
root_cli --verbose --wcsv output --ocsvsep , csv --file input.csv --sep ,
```

The installed CLI application can simply be used by:
where input.csv is:

```
$ <install_path>/bin/root_cli
# or just root_cli if installed in /usr/local/bin/ on unix for instance
```
```
function,method,initial,tolerance,max_iterations,g-function,aitken
x^2-x,fixed_point,0.5,1e-5,100,x^2,true
```

And the shared library can be used inside `cxx` files using:
- CLI input, DAT output file called output.dat, gnuplot writing method (a GNU Plot named output.png is created), Chords method to solve the equation x^3-8 starting from the two initial points 1 and 3:

```
# pass the path of headers
g++ <file>.cpp -o <executable_name> -I<install_path>/include
```
```
root_cli --wgnuplot output cli --function x^3-8 chords --x0 1 --x1 3
```

## Typical program execution

All of which can also be set in `CMakeLists.txt`.
Input reading is handled by a CLI implemented using `CLI11`, which passes the read options to the appropriate `ReaderBase` daughter class. The `read` method of the `ReaderBase` daughter classes construct and return a `ConfigBase` daughter class object. The `ReaderBase` daughter classes also use the `FunctionParserBase` daughter classes internally to parse the function (and derivation + g function) inputted by user (string to a C++ function). The information stored in `ConfigBase` daughter classes is then passed down to the `Solver` class to run the algorithm.

The `solve` method of `Solver` construct and converts a `StepperBase` daughter class object, handles its methods calls, and finally returns the matrix of the results of the computation performed.
`compute_step` method of each `StepperBase` daughter class gets the previous iteration and computes and returns the new guess, which will be saved and checked by `Solver`'s methods.
The final results returned by `solve` are then passed down to the `Writer` class to write them.
Comment thread
Saransh-cpp marked this conversation as resolved.
Outdated

The `write` method of `Writer` construct and converts a `PrinterBase` daughter class object, and handles its methods calls.
`write_values` method of each `StepperBase` daughter class gets a certain value to be printed and prints it out in a defined destination.
Comment thread
Saransh-cpp marked this conversation as resolved.
Outdated

## Tests

Expand Down Expand Up @@ -316,3 +355,13 @@ which will write the HTML files to `docs/html`.
### Building docs on GH Pages

The documentation is automatically built (on every PR) and deployed (on every push to `main`) to GH Pages using the `build-and-deploy-docs` workflow.

## Limitations and problems

Most of the limitations and problems can be found as independent issues in the [issue tracker on GitHub](https://github.com/Saransh-cpp/ROOT/issues), or in the previous Project Structure section.

## Authors and their contributions

- **Andrea Saporito** ([@andreasaporito](https://github.com/andreasaporito)): Stepper, Solver, Writer, Printer classes/functionalities, most of the integration tests and some unit tests, and some fixes/refactoring here and there (touching Reader and Parser classes/functionalities, and the build system).

- **Saransh Chopra** ([@Saransh-cpp](https://github.com/Saransh-cpp)): Top-level CLI executable/application, Reader and Parser classes/functionalities, Project infrastructure (build system {code, docs, tests}, project structure, CI/CD), most of the unit tests and some integration tests, and some refactoring here and there (touching Stepper, Solver, Writer, and Printer classes/functionalities).
3 changes: 3 additions & 0 deletions ROOT/ROOT/config.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
* including Bisection, Newton, Secant, and Fixed Point methods. Each configuration
* class encapsulates the parameters required for its respective method.
*
* This file was written with constant LLM assistance (vibe coded). I built
* the structure and logic, and the LLM helped fill in the details.
*
* @author Saransh-cpp
*
*/
Expand Down
3 changes: 3 additions & 0 deletions ROOT/ROOT/function_parser.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
* including polynomial and trigonometric functions. The parsers convert string representations
* of functions into callable std::function<double(double)> objects.
*
* This file was written with constant LLM assistance (vibe coded). I built
* the structure and logic, and the LLM helped fill in the details.
*
* @author Saransh-cpp
*/
#ifndef FUNCTION_HPP
Expand Down
2 changes: 0 additions & 2 deletions ROOT/ROOT/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,6 @@ int main(int argc, char** argv) {
csv->add_option("--sep", csv_sep, "Separator character for CSV file")->capture_default_str();
char csv_quote = '"';
csv->add_option("--quote", csv_quote, "Quote/delimiter character for CSV file")->capture_default_str();
bool csv_header = true;
csv->add_option("--header", csv_header, "Indicates whether the first row is a header row")->capture_default_str();

// DAT
auto* dat = app.add_subcommand("dat", "Use DAT input");
Expand Down
47 changes: 16 additions & 31 deletions ROOT/ROOT/reader.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,10 @@ std::unique_ptr<ConfigBase> ReaderBase::make_config_from_map(
std::exit(EXIT_FAILURE);
}
double initial = 0.0;
if (!parseDouble(it_x0->second, initial)) {
std::cerr << "\033[31mmake_config_from_map: invalid initial\033[0m\n";
std::exit(EXIT_FAILURE);
}
auto g_function = FunctionParserBase::parseFunction(it_g->second);
return std::make_unique<FixedPointConfig>(tolerance, max_iter, aitken, function, initial, g_function,
verbose);
Expand Down Expand Up @@ -255,7 +259,6 @@ std::unique_ptr<ConfigBase> ReaderCSV::read(CLI::App* app, bool verbose) {
this->filename = app->get_option("--file")->as<std::string>();
this->sep = app->get_option("--sep")->as<char>();
this->quote = app->get_option("--quote")->as<char>();
this->has_header = app->get_option("--header")->as<bool>();
std::ifstream ifs(filename);
if (!ifs) {
std::cerr << "\033[31mReaderCSV: failed to open file: " << filename << "\033[0m\n";
Expand All @@ -265,32 +268,19 @@ std::unique_ptr<ConfigBase> ReaderCSV::read(CLI::App* app, bool verbose) {
std::string headerLine;
std::string valueLine;

if (this->has_header) {
if (!std::getline(ifs, headerLine)) {
std::cerr << "\033[31mReaderCSV: empty file (expecting header)\033[0m\n";
std::exit(EXIT_FAILURE);
}
if (!std::getline(ifs, valueLine)) {
std::cerr << "\033[31mReaderCSV: missing value row\033[0m\n";
std::exit(EXIT_FAILURE);
}
} else {
if (!std::getline(ifs, valueLine)) {
std::cerr << "\033[31mReaderCSV: empty file\033[0m\n";
std::exit(EXIT_FAILURE);
}
headerLine.clear();
if (!std::getline(ifs, headerLine)) {
std::cerr << "\033[31mReaderCSV: empty file (expecting header)\033[0m\n";
std::exit(EXIT_FAILURE);
}
if (!std::getline(ifs, valueLine)) {
std::cerr << "\033[31mReaderCSV: missing value row\033[0m\n";
std::exit(EXIT_FAILURE);
}

std::vector<std::string> headers;
if (this->has_header) {
headers = splitCsvLine(headerLine);
for (auto& header : headers) {
header = trim(header);
}
} else {
std::cerr << "\033[31mReaderCSV: no headers provided\033[0m\n";
std::exit(EXIT_FAILURE);
headers = splitCsvLine(headerLine);
for (auto& header : headers) {
header = trim(header);
}

auto values = splitCsvLine(valueLine);
Expand All @@ -311,13 +301,8 @@ std::unique_ptr<ConfigBase> ReaderCSV::read(CLI::App* app, bool verbose) {
config_map[key] = values[i];
}
} else {
// positional mapping documented here:
std::vector<std::string> posnames = {"method", "tolerance", "max-iterations", "aitken", "function",
"derivative", "interval_a", "interval_b", "function-g", "initial",
"x0", "x1"};
for (size_t i = 0; i < values.size() && i < posnames.size(); ++i) {
config_map[posnames[i]] = values[i];
}
std::cerr << "\033[31mReaderCSV: empty header row\033[0m\n";
std::exit(EXIT_FAILURE);
}

if (verbose) {
Expand Down
4 changes: 3 additions & 1 deletion ROOT/ROOT/reader.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
* which are responsible for reading configuration data from various file formats
* (e.g., CSV, DAT) and producing ConfigBase objects.
*
* This file was written with constant LLM assistance (vibe coded). I built
* the structure and logic, and the LLM helped fill in the details.
*
* @author Saransh-cpp
*/
#ifndef READER_HPP
Expand All @@ -31,7 +34,6 @@ class ReaderBase {
std::string filename; //!< The input filename to read from.
char sep; //!< Field separator character.
char quote; //!< Quote/delimiter character.
bool has_header; //!< Indicates whether the first row is a header row.
/**
* @brief Virtual destructor for ReaderBase.
*
Expand Down
Loading