diff --git a/dotnet-tools.json b/.config/dotnet-tools.json
similarity index 85%
rename from dotnet-tools.json
rename to .config/dotnet-tools.json
index 73f9597..bbab4bd 100644
--- a/dotnet-tools.json
+++ b/.config/dotnet-tools.json
@@ -3,7 +3,7 @@
"isRoot": true,
"tools": {
"dotnet-reportgenerator-globaltool": {
- "version": "5.3.6",
+ "version": "5.4.3",
"commands": [
"reportgenerator"
]
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index eced69d..286d2ce 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -9,8 +9,7 @@ on:
- '**/ci.yaml'
- '**/xunit.runner.json'
branches:
- - main
- - '**release**' # Trigger on branches with 'release' in the name
+ - '**' # Trigger on all branches
pull_request:
paths: # Trigger when source code, sln or prj files, this file, or the xunit config file is changed.
- '**/*.cs'
@@ -42,7 +41,7 @@ jobs:
- name: Setup .NET Core
uses: actions/setup-dotnet@v4
with:
- dotnet-version: '7.x' # Adjust to your target .NET version
+ dotnet-version: '9.x' # Adjust to your target .NET version
# Uncomment this process if you want the windows build to take 3mins extra but get rid of the 'Workload updates are available. Run `dotnet workload list` for more information' msg after building
#- name: Update dotnet workloads (Windows)
@@ -55,15 +54,15 @@ jobs:
- name: Build solution without restore
run: dotnet build tests/EStimLibrary.UnitTests/EStimLibrary.UnitTests.sln --no-restore --configuration Release
- - name: Install ReportGenerator
- run: dotnet tool install -g dotnet-reportgenerator-globaltool
+ - name: Restore .NET Tools
+ run: dotnet tool restore
- name: Run unit tests and collect coverage
run: dotnet test tests/EStimLibrary.UnitTests/EStimLibrary.UnitTests.csproj --no-build --configuration Release --collect:"XPlat Code Coverage"
- name: Generate coverage report
run: |
- reportgenerator -reports:tests/**/coverage.cobertura.xml -targetdir:coverage -reporttypes:Html
+ dotnet tool run reportgenerator -reports:tests/**/coverage.cobertura.xml -targetdir:coverage -reporttypes:Html
- name: Upload coverage report
uses: actions/upload-artifact@v4
diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml
index e6628f8..700305d 100644
--- a/.github/workflows/release.yaml
+++ b/.github/workflows/release.yaml
@@ -24,7 +24,7 @@ jobs:
- name: Setup .NET Core
uses: actions/setup-dotnet@v4
with:
- dotnet-version: '7.x' # Adjust to your target .NET version
+ dotnet-version: '9.x' # Adjust to your target .NET version
- name: Restore dependencies
run: dotnet restore $SLN_PATH
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 0000000..f1884ae
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,7 @@
+{
+ "_comment1": "specify which solution to load for C# Dev Kit tooling. Switch if working on a different sln",
+ "_comment2": "examples/EStimLibrary.ConsoleAppDemo/EStimLibrary.ConsoleAppDemo.sln",
+ "dotrush.roslyn.projectOrSolutionFiles": [
+ "tests/EStimLibrary.UnitTests/EStimLibrary.UnitTests.sln"
+ ]
+}
diff --git a/README.md b/README.md
index 7811b97..49bb23a 100644
--- a/README.md
+++ b/README.md
@@ -1,19 +1,90 @@
# EStimLibrary
A C# library to facilitate electrical stimulation research and development.
-More details coming soon.
+## Table of Contents
+* [Dependencies](#dependencies)
+ * [Extensions for VSCode Users](#extensions-for-vscode-users)
+* [Repo Contents](#repo-contents)
+ * [Top-Level Structures](#top-level-structure)
+ * [Projects and Solutions](#projects-and-solutions)
+* [Library Structure](#library-structure)
+* [Diagrams](#diagrams)
+* [Usage](#usage)
+ * [VisualStudio](#visualstudio)
+ * [VSCode or your other preferred IDE](#vscode-or-your-other-preferred-ide)
-## Repo Structure
-* `src/` - the library source code
-* `tests/` - the test projects
+## Dependencies
+* .NET 9.0
+ * 10-minute [Tim Corey video](https://www.youtube.com/watch?v=sXEsvqCCTTc): how to upgrade or install fresh
+ * Windows: Use VisualStudio
+ * MacOS, Linux: download and install manually to access `dotnet` command-line interface (CLI)
+* `nuget` packages:
+ * For base library:
+ * System.IO.Ports
+ * MathNet.Numerics
+ * Newtonsoft.Json
+ * For xUnit testing project:
+ * Microsoft.NET.Test.Sdk
+ * xunit
+ * xunit.runner.visualstudio
+ * xunit.runner.console
+ * coverlet.collector
+ * Versions are listed in the `.csproj` files and should be pulled automatically when building projects and solutions in this repo.
+
+### Extensions for VSCode Users
+Optional but highly recommended:
+* C# Dev Kit: provides much of the same C# support and within-solution navigation features as VisualStudio
+
+## Repo Contents
+### Top-Level Structure
+* `src/` - the library source code project
+* `tests/` - the test project(s)
* `models/` - examples of spatial model definition files
-* `examples/` - example applications using the library's high-level API
+ * contains [`EStimLibrary.SpatialModels`](https://github.com/lhmcgann/EStimLibrary.SpatialModels/) as a submodule
+* `examples/` - example applications (projects) using the library's high-level API
* `docs/` - additional documentation files, including a dictionary and coding conventions
-## UML Diagrams
-Last updated: March 2024
-* [Class Diagram](https://lucid.app/lucidchart/298700bf-4e2e-4c7d-bb06-76365f1efb98/edit?viewport_loc=-7329%2C-1594%2C17982%2C8602%2C0_0&invitationId=inv_11b76774-377e-4967-87a5-c173614a4ef3)
-* [Sequence and Use Case Diagrams](https://lucid.app/lucidchart/e774e654-27e9-4168-aee4-39f6f8133738/edit?viewport_loc=-403%2C-342%2C1628%2C779%2C0_0&invitationId=inv_cb3b6cf3-cb84-4091-bd72-88547d50cb3e)
+### Projects and Solutions
+There are currently 3 projects in this repo:
+1. `src/EStimLibrary/EStimLibrary.csproj`: the base library project itself
+ * Builds into a `.dll`
+2. `tests/EStimLibrary.UnitTests/EStimLibrary.UnitTests.csproj`: the xUnit test project
+ * The unit tests for the library
+3. `examples/EStimLibrary.ConsoleAppDemo/EStimLibrary.ConsoleAppDemo.csproj`: an example console app project that either runs through a hard-coded session config and execution, or walks through the config step-by-step, asking for user input at each step
+ * Highlights the `Utils` reflection capabilities,
+ * and exemplifies the major `HapticSession` config components.
+
+There are 3 corresponding solutions in this repo:
+1. `EStimLibrary.sln`: builds only the base library project
+2. `tests/EStimLibrary.UnitTests/EStimLibrary.UnitTests.sln`: builds the base library and the xUnit test projects
+3. `examples/EStimLibrary.ConsoleAppDemo/EStimLibrary.ConsoleAppDemo.sln`: builds the base library and the example console app projects
+
+## Library Structure
+The main library, in `src/EStimLibrary/`, is broken down into two top-level folders:
+* `Core/`: the base declarations and implementatsions of interfaces, abstract classes, and core classes
+* `Extensions/`: provided implementations of some core interfaces and abstract classes that are commonly used
+
+The `Core/` folder contains the following structure:
+* `Data/`: primarily `IDataLimits`
+* `Haptics/`: classes on the haptic event side of the pipeline, e.g., `HapticSession`, `HapticEvent`, and `HapticTransducer`
+* `HardwareInterfaces/`: leads, cables, neural interfaces
+* `SpatialModel/`: location, area, body model, and related content
+* `Stimulation/`: stimulators and stimulation representations
+* Miscellaneous infrastructure interfaces and classes are in the top level of the `Core/` folder, e.g., `Utils`, `ResourceManager`, `ReusableIdPool`
+
+The `Extensions/` folder mimics this structure to the extent that example implementations are provided.
+
+The `EStimLibrary.UnitTests` project also mimics the `Core/`, `Extensions/`, and sub-directory structures.
+
+## Diagrams
+Last updated: February 2025
+
+These diagrams are based on UML formatting but adhere to that to varying degrees.
+* [Key Components](https://lucid.app/lucidchart/7b9b75ff-8dee-41d8-b650-082adc9bcb8f/edit?viewport_loc=-1212%2C-695%2C5508%2C2636%2C0_0&invitationId=inv_0a3eaaf0-7a4f-4fcd-a813-b786f47e548f)
+* [Class Diagram - High-Level](https://lucid.app/lucidchart/1366a885-086c-45d0-8763-63448fe11a86/edit?viewport_loc=-3539%2C-4330%2C16143%2C7727%2C0_0&invitationId=inv_10ca78f7-8fcc-4d34-8466-aa8da86d8716)
+* [`StringHierarchy` Class Diagram](https://lucid.app/lucidchart/8f0c6f70-c343-444e-9e6f-5c51557384ec/edit?viewport_loc=-2888%2C-1102%2C7382%2C3534%2C0_0&invitationId=inv_7e104f4d-b695-4ff7-84b2-87b3bff9a9fb)
+* [Sequence Diagrams](https://lucid.app/lucidchart/312bfb4e-807b-4f90-9e56-7cdc64a0172d/edit?viewport_loc=-858%2C313%2C2104%2C917%2C0_0&invitationId=inv_94e9dfe8-1656-45c9-abc7-d433c77d7a95)
+* [Use Case Diagrams](https://lucid.app/lucidchart/85097f76-39d7-49a4-b1a6-5fa749e0ad9f/edit?viewport_loc=-3588%2C-303%2C10396%2C4532%2C0_0&invitationId=inv_c1be5bbe-3b3e-4b08-b021-a66debdfbed8)
Beneficial in the future:
* package diagram showing the interface exposed to the public
@@ -23,3 +94,27 @@ Beneficial in the future:
See [UML.md](./docs/UML.md) for a UML intro.
+
+## Usage
+To use the library, clone the repo and checkout the `release-v2.0` branch for the most recent development:
+```
+# To clone submodules as well: call git clone with the --recurse-submodules option if you have Git 2.13 or later, else --recursive
+git clone git@github.com:lhmcgann/EStimLibrary.git
+cd EStimLibrary
+git checkout release-v2.0
+```
+
+### VisualStudio
+1. Open the `.sln` file corresponding to the projects you want to open.
+2. Press the "play" button in the top left to build (and run, if applicable) the loaded solution.
+3. To run the test project, click the green "play" button in Test Explorer.
+
+### VSCode or your other preferred IDE
+1. Open the `EStimLibrary/` folder in your workspace.
+2. In Terminal, navigate to the folder with the desired project or solution.
+3. Use `dotnet` CLI commands. See a cheatsheet [here](https://www.lostindetails.com/articles/dotnet-cheatsheet).
+ 1. `dotnet build` to build any project
+ 2. `dotnet run` to run the example console app
+ 3. `dotnet test` to run the xUnit test suite
+
+
diff --git a/docs/images/association_arrows-techvidvan.png b/docs/images/association_arrows-techvidvan.png
index 7642f51..54ff5ff 100644
Binary files a/docs/images/association_arrows-techvidvan.png and b/docs/images/association_arrows-techvidvan.png differ
diff --git a/docs/images/association_multiplicity_annotations-vertabelo.png b/docs/images/association_multiplicity_annotations-vertabelo.png
index 9a00cb8..0d2d1d6 100644
Binary files a/docs/images/association_multiplicity_annotations-vertabelo.png and b/docs/images/association_multiplicity_annotations-vertabelo.png differ
diff --git a/docs/images/association_multiplicity_concept-techvidvan.png b/docs/images/association_multiplicity_concept-techvidvan.png
index 4379615..ca68409 100644
Binary files a/docs/images/association_multiplicity_concept-techvidvan.png and b/docs/images/association_multiplicity_concept-techvidvan.png differ
diff --git a/docs/images/class_notation-tutorialspoint.jpeg b/docs/images/class_notation-tutorialspoint.jpeg
index 198e734..ef737b2 100644
Binary files a/docs/images/class_notation-tutorialspoint.jpeg and b/docs/images/class_notation-tutorialspoint.jpeg differ
diff --git a/docs/images/uml_arrows-stack_overflow.png b/docs/images/uml_arrows-stack_overflow.png
index 3fd8533..56809c2 100644
Binary files a/docs/images/uml_arrows-stack_overflow.png and b/docs/images/uml_arrows-stack_overflow.png differ
diff --git a/examples/EStimLibrary.ConsoleAppDemo/EStimLibrary.ConsoleAppDemo.csproj b/examples/EStimLibrary.ConsoleAppDemo/EStimLibrary.ConsoleAppDemo.csproj
index c47af07..eb43873 100644
--- a/examples/EStimLibrary.ConsoleAppDemo/EStimLibrary.ConsoleAppDemo.csproj
+++ b/examples/EStimLibrary.ConsoleAppDemo/EStimLibrary.ConsoleAppDemo.csproj
@@ -2,7 +2,7 @@
Exe
- net7.0
+ net9.0enableenable
diff --git a/examples/EStimLibrary.ConsoleAppDemo/Program.cs b/examples/EStimLibrary.ConsoleAppDemo/Program.cs
index 5a44cf7..296aa60 100644
--- a/examples/EStimLibrary.ConsoleAppDemo/Program.cs
+++ b/examples/EStimLibrary.ConsoleAppDemo/Program.cs
@@ -660,12 +660,14 @@
"left hand, index finger, distal phalanx | palmar");
foreach (var pVal in pressureValues)
{
- var pNormVec = Vector.Build.Dense(new double[] { pVal });
+ //var pNormVec = Vector.Build.Dense(new double[] { pVal });
+ var hapticParams = new Dictionary();
+ hapticParams.Add(HapticParam.P, pVal);
var event1 = new HapticEvent(DateTime.Now, null,
new() {
{ "left hand", new List() { eventArea1 } }//, eventArea2 } }
},
- pNormVec);
+ hapticParams);
session.AddEvent(event1);
}
session.Stop();
diff --git a/src/EStimLibrary/Core/Data/IDataLimits.cs b/src/EStimLibrary/Core/Data/IDataLimits.cs
new file mode 100644
index 0000000..3a2462c
--- /dev/null
+++ b/src/EStimLibrary/Core/Data/IDataLimits.cs
@@ -0,0 +1,28 @@
+namespace EStimLibrary.Core.Data;
+
+
+///
+/// A structure to represent the limits on a specific associated parameter or
+/// other data value, supplying data type information and bounding/limiting
+/// validation.
+///
+public interface IDataLimits : ISelectable
+{
+ ///
+ /// The data type that is valid for the associated data.
+ ///
+ public Type ValidDataType { get; }
+ ///
+ /// A strign description of the limits this structure imposes on the
+ /// associated data. Meant to be open-ended to accommodate listing discrete
+ /// options, explaining bounds, etc. Basically a 'help' message.
+ ///
+ public string Description { get; }
+ ///
+ /// Validate a specific data value. Must be non-null and of the
+ /// ValidDataType, in addition to any other validation checks.
+ ///
+ ///
+ /// True if the value is valid, false if not.
+ bool IsValidDataValue(object value);
+}
\ No newline at end of file
diff --git a/src/EStimLibrary/Core/DataLimits.cs b/src/EStimLibrary/Core/DataLimits.cs
deleted file mode 100644
index 2d04ef3..0000000
--- a/src/EStimLibrary/Core/DataLimits.cs
+++ /dev/null
@@ -1,249 +0,0 @@
-namespace EStimLibrary.Core;
-
-
-///
-/// A structure to represent the limits on a specific associated parameter or
-/// other data value, supplying data type information and bounding/limiting
-/// validation.
-///
-public interface IDataLimits : ISelectable
-{
- ///
- /// The data type that is valid for the associated data.
- ///
- public Type ValidDataType { get; }
- ///
- /// A strign description of the limits this structure imposes on the
- /// associated data. Meant to be open-ended to accommodate listing discrete
- /// options, explaining bounds, etc. Basically a 'help' message.
- ///
- public string Description { get; }
- ///
- /// Validate a specific data value. Must be non-null and of the
- /// ValidDataType, in addition to any other validation checks.
- ///
- ///
- /// True if the value is valid, false if not.
- bool IsValidDataValue(object value);
-}
-
-public record StringDataLimits : IDataLimits
-{
- public string Name => "String Data Limits";
-
- public Type ValidDataType => typeof(string);
- public string Description => "Any string-type data.";
- public bool IsValidDataValue(object value)
- {
- return value.GetType() == this.ValidDataType;
- }
-}
-
-///
-/// A limit structure for data that can only have values from a specific list.
-///
-/// The data type of the specific options for the
-/// associated data.
-/// The specific values the associated data can be.
-///
-public record FixedOptionDataLimits(
- SortedSet DataOptions) : IDataLimits
-{
- public string Name => "Fixed Option Data Limits";
- public Type ValidDataType => typeof(DataType);
- public string Description => $"Data of type '{typeof(DataType)}' with " +
- $"discrete options:\n\t" +
- string.Join($"\n\t", this.DataOptions);
-
- public bool IsValidDataValue(object value)
- {
- return value is not null &&
- value.GetType() == this.ValidDataType &&
- this.DataOptions.Contains((DataType)value);
- }
-}
-
-///
-/// A limit structure for data that can have values within a continuous,
-/// real-valued range.
-///
-/// The lower bound, inclusive.
-/// The upper bound, inclusive.
-public record ContinuousDataLimits(double MinBound, double MaxBound) :
- IDataLimits
-{
- public string Name => "Continuous Data Limits";
- public Type ValidDataType => typeof(double);
- public string Description => $"{this.Name}: [{this.MinBound}, " +
- $"{this.MaxBound}]";
-
- public bool IsValidDataValue(object value)
- {
- return value is not null &&
- value.GetType() == this.ValidDataType &&
- this.MinBound <= (double)value &&
- (double)value <= this.MaxBound;
- }
-}
-
-///
-/// A limit structure for data that can have values within a continuous,
-/// integer-valued range.
-///
-/// The lower bound, inclusive.
-/// The upper bound, inclusive.
-public record ContinuousIntDataLimits(int MinBound, int MaxBound) :
- IDataLimits
-{
- public string Name => "Continuous Integer Data Limits";
- public Type ValidDataType => typeof(int);
- public string Description => $"{this.Name}: [{this.MinBound}, " +
- $"{this.MaxBound}]";
-
- public bool IsValidDataValue(object value)
- {
- return value is not null &&
- value.GetType() == this.ValidDataType &&
- this.MinBound <= (int)value &&
- (int)value <= this.MaxBound;
- }
-}
-
-///
-/// A limit structure for data with run-time-dependent limits or some other form
-/// of dynamic validation.
-///
-/// The data type of the associated data.
-/// The dynamic validation function that is called
-/// within the IsValidDataValue() method of this limits record. The function
-/// must take in the data value (of the correct data type) and return a boolean
-/// indicating if the data value is valid at the time when the function is
-/// called.
-/// The string description of the dynamic validation.
-/// Will be used as the overall description for the limits imposed.
-public record DynamicDataLimits(
- Func CheckFunction, string Description) : IDataLimits
-{
- public string Name => "Dynamic Data Limits";
- public Type ValidDataType => typeof(DataType);
-
- public bool IsValidDataValue(object value)
- {
- return value is not null &&
- value.GetType() == this.ValidDataType &&
- this.CheckFunction((DataType)value);
- }
-}
-
-///
-/// A limit structure for data that is itself a sequence of data values, each
-/// element with its own subsequent data limits.
-///
-/// A list of string names for each element in
-/// the sequence. The names must be unique and in the desired order.
-/// The corresponding data limits for each element,
-/// keyed by the element's string name which must be present in
-/// OrderedElementNames.
-/// An optional parameter: the string names of
-/// any elements in the sequence that are themselves optional and don't have to
-/// be included in an actual sequence data record. This set must only contain
-/// strings found in OrderedElementNames. Value is null by default, i.e., if all
-/// elements are required.
-public record SequenceDataLimits(List OrderedElementNames,
- Dictionary ElementLimits,
- IEnumerable OptionalElements = null) : IDataLimits
-{
- public string Name => "Sequence Data Limits";
- // At bare minimum, data instances valid for further validation of this
- // against this sequence limitations are ordered lists of sequence element
- // name-value pairs.
- public Type ValidDataType => typeof(List>);
- public string Description
- {
- get
- {
- string description = "A sequence of data values, each with their " +
- $"own data limits, as follows:\n";
- // Add the description of each element's data limits.
- for (int i = 0; i < this.OrderedElementNames.Count; i++)
- {
- string elementName = this.OrderedElementNames[i];
-
- // Add a '*' to the beginning of the element description if it
- // is an optional element in the sequence.
- bool isOptional = this.OptionalElements is not null && this.OptionalElements.Contains(elementName);
- string optionalPrefix = isOptional ? "*" : "";
-
- description = description + $"\t{optionalPrefix}" +
- $"Element {i + 1} = '{elementName}': " +
- $"{this.ElementLimits[elementName].Description}\n";
- }
- return description;
- }
- }
-
- public bool IsValidDataValue(object value)
- {
- // Fail immediately if null or not a list of name-value pairs is given.
- if (value is null || value.GetType() != this.ValidDataType)
- {
- return false;
- }
-
- // Extract the sequence, a list of element name-value pairs.
- var sequence = (List>)value;
-
- // Iterate through each element name-value pair.
- int headerIndex = 0;
- for (int dataIndex = 0; dataIndex < sequence.Count; dataIndex++)
- {
- // Return failure if data values provided but no more expected
- // in the sequence.
- if (headerIndex >= this.OrderedElementNames.Count)
- {
- return false;
- }
-
- // Get the element name-value pair.
- (string givenName, object givenValue) = sequence[dataIndex];
-
- // Get the name of the element expected to be at this index.
- var expectedName = this.OrderedElementNames[headerIndex];
-
- // Check if the expected and given names match.
- if (!givenName.Equals(expectedName))
- {
- // If names don't match, check if the expected element is
- // optional. Simply go to next header index if so.
- if (this.OptionalElements is not null &&
- this.OptionalElements.Contains(expectedName))
- {
- // Decrement the data index so this value is checked again
- // and not skipped.
- dataIndex--;
- }
- // Otherwise, return failure: the sequence is missing an
- // element.
- else
- {
- return false;
- }
- }
-
- // Otherwise, the names match, so get the data limits.
- var limits = this.ElementLimits[givenName];
- // Return failure if an invalid element value is given.
- if (!limits.IsValidDataValue(givenValue))
- {
- return false;
- }
-
- // Otherwise, it's valid data so far. Look at the next expected
- // value.
- headerIndex++;
- }
-
- // If made it to the end of the loop, all data valid. Return success.
- return true;
- }
-}
\ No newline at end of file
diff --git a/src/EStimLibrary/Core/Haptics/HapticEvent.cs b/src/EStimLibrary/Core/Haptics/HapticEvent.cs
index 5159a2a..dbe500f 100644
--- a/src/EStimLibrary/Core/Haptics/HapticEvent.cs
+++ b/src/EStimLibrary/Core/Haptics/HapticEvent.cs
@@ -24,19 +24,13 @@ namespace EStimLibrary.Core.Haptics;
/// conforming to the body model. Parameter may be null or contain otherwise
/// invalid data if LocalizeByArea is false.
///
-///
+/// The dictionary of parameters for the haptic
+/// event. Key is the haptic param label (see HapticParamEnum).
/// True if this event should be localized on the
/// body model using this event's Area, False if this event should be localized
/// on the body model using this event's Location.
public record HapticEvent(DateTime Timestamp,
Dictionary> Locations,
Dictionary> Areas,
- Vector HapticParamData,
- bool LocalizeByArea = true);
-
-// TODO: add labels/headers to stim params. Could either change this record to
-// by default include the below (but need to figure out how to do with enums and
-// allowing extension), or add a record inheriting from this one that is a
-// ParameterizedHapticEvent. If did add this, would need to add a config step to
-// select haptic params.
-// public SortedSet HapticParams { get; init; }
\ No newline at end of file
+ Dictionary HapticParamData,
+ bool LocalizeByArea = true);
\ No newline at end of file
diff --git a/src/EStimLibrary/Core/Haptics/HapticSession.cs b/src/EStimLibrary/Core/Haptics/HapticSession.cs
index 8bf0163..5ed2181 100644
--- a/src/EStimLibrary/Core/Haptics/HapticSession.cs
+++ b/src/EStimLibrary/Core/Haptics/HapticSession.cs
@@ -466,7 +466,7 @@ public bool TryMapLeadPool(SortedSet leadIds, string bodyModelKey,
this._stimManager.TryGetStimulator(stimId, out var stimulator);
// Create config data: [stimId, leads, availableParams]
ThreadConfigDataPerStimulator data = new(stimId, stimLeads,
- stimulator.StimParamData, stimulator.ModulatableStimParams);
+ stimulator.StimParamSpecs, stimulator.ModulatableStimParams);
// Add to total set of config data.
allConfigData.Add(stimId, data);
}
diff --git a/src/EStimLibrary/Core/Haptics/HapticTransducer.cs b/src/EStimLibrary/Core/Haptics/HapticTransducer.cs
index 2087606..2b14249 100644
--- a/src/EStimLibrary/Core/Haptics/HapticTransducer.cs
+++ b/src/EStimLibrary/Core/Haptics/HapticTransducer.cs
@@ -9,6 +9,8 @@ public abstract class HapticTransducer : ISelectable
{
public abstract string Name { get; } // ISelectable
+ // TODO: property that says which parameters are modulated
+
///
/// Transduce a haptic event into stimulation data and push the data changes
/// to stimulator hardware. A new programmatic thread is created for each
diff --git a/src/EStimLibrary/Core/HardwareInterfaces/Lead.cs b/src/EStimLibrary/Core/HardwareInterfaces/Lead.cs
index 9a155e3..2e0d4db 100644
--- a/src/EStimLibrary/Core/HardwareInterfaces/Lead.cs
+++ b/src/EStimLibrary/Core/HardwareInterfaces/Lead.cs
@@ -1,7 +1,4 @@
-using EStimLibrary.Core;
-
-
-namespace EStimLibrary.Core.HardwareInterfaces;
+namespace EStimLibrary.Core.HardwareInterfaces;
///
@@ -21,9 +18,18 @@ public record Lead(SortedSet ContactSet, SortedSet OutputSet,
Constants.CurrentDirection CurrentDirection) :
IIdentifiable
{
- // Manager-given ID of the lead, -1 if unset.
- public int Id => this._Id; // IIdentifiable
- internal int _Id = -1; // to be set by the manager.
+
+ ///
+ /// The manager-given ID of the lead.
+ ///
+ /// The ID of the lead, or -1 if unset.
+ public int Id => this._Id; // IIdentifiable
+
+ ///
+ /// Internal storage for the lead ID, to be set by the manager.
+ ///
+ /// The ID of the lead, default is -1.
+ internal int _Id = -1; // to be set by the manager.
///
/// Get which outputs are connected to a given output or contact by this
@@ -44,11 +50,12 @@ public bool GetConnectedOutputs(int id, bool searchIsAnOutput,
// Output the set of outputs even if the requested ID is invalid.
connectedOutputs = new(this.OutputSet);
- var validId = false;
+ bool validId;
// Search by output or contact ID, respectively.
- if (searchIsAnOutput && (validId = this.OutputSet.Contains(id)))
+ if (searchIsAnOutput)
{
// Exclude the search output ID from the returned set.
+ validId = this.OutputSet.Contains(id);
connectedOutputs.ExceptWith(new int[] { id });
}
else
@@ -71,7 +78,7 @@ public bool GetConnectedOutputs(int id, bool searchIsAnOutput,
/// contact, False if the given search ID is of an output.
/// An output parameter: the set of
/// contacts connected to the searched contact or output. If a contact was
- /// searched, the set will exclude that contact. If the method returns False,
+ /// searched, the set will exclude that contact. If the method returns False
/// this will just be the set of all contacts in this Lead.
/// True if the given search ID was found in this Lead and the
/// returned contact ID set is valid, False if not.
@@ -81,10 +88,11 @@ public bool GetConnectedContacts(int id, bool searchIsAContact,
// Output the set of contacts even if the requested ID is invalid.
connectedContacts = new(this.ContactSet);
- var validId = false;
+ bool validId;
// Search by contact or output ID, respectively.
- if (searchIsAContact && (validId = this.ContactSet.Contains(id)))
+ if (searchIsAContact)
{
+ validId = this.ContactSet.Contains(id);
// Exclude the search contact ID from the returned set.
connectedContacts.ExceptWith(new int[] { id });
}
diff --git a/src/EStimLibrary/Core/HardwareInterfaces/LeadManager.cs b/src/EStimLibrary/Core/HardwareInterfaces/LeadManager.cs
index e9c8158..04e5912 100644
--- a/src/EStimLibrary/Core/HardwareInterfaces/LeadManager.cs
+++ b/src/EStimLibrary/Core/HardwareInterfaces/LeadManager.cs
@@ -30,7 +30,7 @@ public class LeadManager : ResourceManager
//TODO? public Dictionary ContactOutputMap { get; protected set; }
- public LeadManager()
+ public LeadManager() : base()
{
// Initialize wired ID sets.
this._WiredContacts = new();
diff --git a/src/EStimLibrary/Core/IFactory.cs b/src/EStimLibrary/Core/IFactory.cs
index ef0f6ae..033c374 100644
--- a/src/EStimLibrary/Core/IFactory.cs
+++ b/src/EStimLibrary/Core/IFactory.cs
@@ -1,4 +1,7 @@
-namespace EStimLibrary.Core;
+using EStimLibrary.Core.Data;
+
+
+namespace EStimLibrary.Core;
public interface IFactory
diff --git a/src/EStimLibrary/Core/ILimitable.cs b/src/EStimLibrary/Core/ILimitable.cs
index 1aa7430..f9fc984 100644
--- a/src/EStimLibrary/Core/ILimitable.cs
+++ b/src/EStimLibrary/Core/ILimitable.cs
@@ -1,4 +1,4 @@
-using EStimLibrary.Core;
+using EStimLibrary.Core.Data;
namespace EStimLibrary;
diff --git a/src/EStimLibrary/Core/SpatialModel/BodyModelBuilderBase.cs b/src/EStimLibrary/Core/SpatialModel/BodyModelBuilderBase.cs
index b56a09a..833e98b 100644
--- a/src/EStimLibrary/Core/SpatialModel/BodyModelBuilderBase.cs
+++ b/src/EStimLibrary/Core/SpatialModel/BodyModelBuilderBase.cs
@@ -1,4 +1,8 @@
-namespace EStimLibrary.Core.SpatialModel;
+using EStimLibrary.Core.Data;
+using EStimLibrary.Extensions.Data;
+
+
+namespace EStimLibrary.Core.SpatialModel;
public abstract class BodyModelBuilderBase : ISelectable, IFactory
diff --git a/src/EStimLibrary/Core/SpatialModel/IBodyModel.cs b/src/EStimLibrary/Core/SpatialModel/IBodyModel.cs
index 98cb7b9..cabbf8e 100644
--- a/src/EStimLibrary/Core/SpatialModel/IBodyModel.cs
+++ b/src/EStimLibrary/Core/SpatialModel/IBodyModel.cs
@@ -1,4 +1,4 @@
-using EStimLibrary.Core;
+using EStimLibrary.Core.Data;
namespace EStimLibrary.Core.SpatialModel;
diff --git a/src/EStimLibrary/Core/Stimulation/Data/BaseStimParams.cs b/src/EStimLibrary/Core/Stimulation/Data/BaseStimParams.cs
deleted file mode 100644
index 0ce7f65..0000000
--- a/src/EStimLibrary/Core/Stimulation/Data/BaseStimParams.cs
+++ /dev/null
@@ -1,171 +0,0 @@
-using EStimLibrary.Extensions.Stimulation.Phases;
-using EStimLibrary.Core;
-
-
-namespace EStimLibrary.Core.Stimulation.Data;
-
-// TODO: DELETE once implemented w/ individual enums
-// TODO: OR make these Flags so can use bit arithmetic and flagging principles,
-// along with the SortedSet list of params available or used, to mark
-// simultaneously when and which parameters are changed
-
-// TODO: describe each param more as a flag for something the stimulator can do,
-// rather than as a data value, even though the two may correspond
-
-//public enum BaseStimParam
-//{
-// // Phase Params
-// PA, // phase amplitude-- the amplitude in mA of the cathodic pulse phase
-// PW, // phase width-- the width in us of the cathodic pulse phase
-// PhaseShape, // phase shape-- refers to the shape of each phase of the pulse (default is rectangular)
-
-// // Pulse Params
-// IPD, // interphase delay-- the time in us between two phases of a pulse
-// AnodeRatio, // anode ratio-- the multiplier of the anodic phase's PW/divisor of the PA for charge balance. ex: AR = 2 -> anodic phase is twice the PW and half the PA of the cathodic phase
-// AnodeFirst, // anode first-- binary that indicates that the anodic phase of the biphasic pulse should go first (default 0 is cathode-first)
-
-// // Pattern Params
-// Period, // pattern period-- the period in ms of a pattern; all pulses (and the delays between them) in the pattern must fit within this time window; classically 1/PF given a pattern with only 1 pulse
-
-// // Train Params
-// FixedRepeats // number of repeats-- the number of pattern/period repeats this train should run for
-//}
-
-///
-/// A static class of *Extension Methods* to check which sub-group a stim param
-/// falls under.
-/// Ref.: https://stackoverflow.com/questions/9299279/how-to-group-enum-values
-///
-public static class BaseStimParams
-{
- public const string PA = "PA";
- public const string PW = "PW";
- public const string PhaseShape = "PhaseShape";
- public const string IPD = "IPD";
- public const string AnodeRatio = "AnodeRatio";
- public const string AnodeFirst = "AnodeFirst";
- public const string Period = "Period";
- public const string FixedRepeats = "FixedRepeats";
-
- public static Dictionary ParamOrderIndices = new()
- {
- {PA, 0 },
- {PW, 1 },
- {PhaseShape, 2 },
- {IPD, 3 },
- {AnodeRatio, 4 },
- {AnodeFirst, 5 },
- {Period, 6 },
- {FixedRepeats, 7 }
- };
-
- // TODO: put in the actual limits for each param; rn just semi-dummy values
- public static Dictionary>
- ExampleParamData => new()
- {
- // Phase amplitude in mA
- { PA, new(
- new ContinuousDataLimits(0.0, 100),
- 12.0)},
- // Phase width in us
- { PW, new(
- new ContinuousDataLimits(1.0, 250.0),
- 4.0)},
- // Phase shape
- { PhaseShape, new(
- new FixedOptionDataLimits(new()
- {
- typeof(SquarePhaseData)
- }),
- typeof(SquarePhaseData))},
- // Inter-phase delay in us
- { IPD, new(
- new ContinuousDataLimits(1.0, 10.0),
- 1.0)},
- { AnodeRatio, new(
- new ContinuousDataLimits(0.0, 10.0),
- 8.0)},
- { AnodeFirst, new(
- new FixedOptionDataLimits(new()
- {
- Constants.ANODE_FIRST,
- Constants.ANODE_SECOND
- }),
- Constants.ANODE_SECOND)},
- // Pulse period in s (1/Hz)
- { Period, new(
- new ContinuousDataLimits(0.0, 1/250.0),
- 1/60.0)},
- { FixedRepeats, new(
- new ContinuousIntDataLimits(0, Constants.POS_INFINITY),
- 10)}
- };
-
- //public const int FirstPhaseParamIdx = (int)BaseStimParam.PA;
- //public const int FirstPulseParamIdx = (int)BaseStimParam.IPD;
- //public const int FirstPatternParamIdx = (int)BaseStimParam.Period;
- //public const int FirstTrainParamIdx = (int)BaseStimParam.FixedRepeats;
-
- public static bool IsPhaseParam(string param)
- {
- switch (param)
- {
- case BaseStimParams.PA:
- case BaseStimParams.PW:
- case BaseStimParams.PhaseShape:
- return true;
-
- default:
- return false;
- }
- }
-
- public static bool IsPulseParam(string param)
- {
- switch (param)
- {
- case BaseStimParams.IPD:
- case BaseStimParams.AnodeRatio:
- case BaseStimParams.AnodeFirst:
- return true;
-
- default:
- return false;
- }
- }
-
- public static bool IsPatternParam(string param)
- {
- switch (param)
- {
- case BaseStimParams.Period:
- return true;
-
- default:
- return false;
- }
- }
-
- public static bool IsTrainParam(string param)
- {
- switch (param)
- {
- case BaseStimParams.FixedRepeats:
- return true;
-
- default:
- return false;
- }
- }
-
- ///
- /// Sort parameter string keys by paired integer value.
- ///
- /// The parameter key: order index pairs.
- /// An ordered list of parameter keys.
- public static List SortParams(Dictionary paramIndices)
- {
- return paramIndices.OrderBy(pair => pair.Value)
- .Select(pair => pair.Key).ToList();
- }
-}
diff --git a/src/EStimLibrary/Core/Stimulation/StimulatorManager.cs b/src/EStimLibrary/Core/Stimulation/StimulatorManager.cs
index 5a19169..8f08ac9 100644
--- a/src/EStimLibrary/Core/Stimulation/StimulatorManager.cs
+++ b/src/EStimLibrary/Core/Stimulation/StimulatorManager.cs
@@ -55,12 +55,6 @@ public StimulatorManager() : base()
this._OutputIdPool = new();
this._OutputStimulatorIdMap = new();
this._OutputsPerStimulatorUsage = new();
-
- // Create stimulator ability dict with empty sets for all stim params.
- foreach (string p in BaseStimParams.ParamOrderIndices.Keys)
- {
- this._stimulatorsWithAbilities.Add(p, new());
- }
}
///
@@ -113,6 +107,13 @@ public bool TryCreateAndRegisterStimulator(Type stimulatorType,
// 4) Add the new stim ID to the set of each pulse param it supports.
foreach (string p in stim.ModulatableStimParams)
{
+ // If new param, add an empty set entry for it in the dictionary
+ if (!this._stimulatorsWithAbilities.ContainsKey(p))
+ {
+ this._stimulatorsWithAbilities[p] = new();
+ }
+
+ // Add the global stimulator ID
this._stimulatorsWithAbilities[p].Add(globalStimId);
}
@@ -368,6 +369,7 @@ public void UpdateStim(StimThread stimThread)
// the value of Tuple(trains, globalToLocalOutputIds).
//ThreadPool.QueueUserWorkItem(state => stim.UpdateStim(state),
// (trains, globalToLocalOutputIds));
+ // TODO: try-catch exception here; make error user-accessible but continue on safely
stim.UpdateStim((trainsParams, localOutputAssignments));
}
// Do nothing if invalid stim ID. TODO: how best to indicate this
diff --git a/src/EStimLibrary/Core/Stimulation/Stimulators/Stimulator.cs b/src/EStimLibrary/Core/Stimulation/Stimulators/Stimulator.cs
index e37287f..4b46771 100644
--- a/src/EStimLibrary/Core/Stimulation/Stimulators/Stimulator.cs
+++ b/src/EStimLibrary/Core/Stimulation/Stimulators/Stimulator.cs
@@ -1,6 +1,6 @@
using EStimLibrary.Core.Stimulation.Data;
using EStimLibrary.Core.Stimulation.Functions;
-using EStimLibrary.Core.Stimulation.Trains;
+using EStimLibrary.Core.Data;
namespace EStimLibrary.Core.Stimulation.Stimulators;
@@ -35,26 +35,51 @@ protected abstract ValidateStimParamDataDelegate StimParamDataCheckFunction
// Derived classes must implement the get() of the abstract properties.
// e.g., SpecificStimulator.NumOutputs get { return constNumOutputs; }.
- public abstract Dictionary> StimParamData
+ // TODO: define (in Extensions) a type that is a NestedParam; maybe it's a
+ // nested param DataLimits, idk, but a tool ppl who are running into the
+ // "trainMods: List" case can use; maybe it's just an example
+ // implementation, but something
+
+ ///
+ /// The definition of available stimulation parameters on the stimulator,
+ /// their data limits, and their default/fixed value.
+ /// {"paramNameOrKey": (dataLimitsObject, defaultOrFixedValue), ...}
+ /// typeof(defaultOrFixedValue) must match dataLimitsObject.ValidDataType.
+ /// dataLimitsObject.IsValidDataValue(defaultOrFixedValue) must return True.
+ ///
+ public abstract Dictionary>
+ StimParamSpecs
{ get; }
+
+ ///
+ /// The set of string keys of stimluation parameters that can be modulated
+ /// on the fly at run-time. All keys specified must be in StimParamSpecs.
+ ///
public abstract SortedSet ModulatableStimParams
{ get; }
- // Fixed-value params must be all params available that aren't modulatable.
+
+ ///
+ /// All stimulation parameters available that are not modulatable on the fly
+ /// at run-time. All keys listed will be in StimParamSpecs.
+ ///
public SortedSet FixedStimParams =>
new(this._StimParamsAvailable.Except(this.ModulatableStimParams));
- //public abstract Dictionary FixedStimParamValues
- //{ get; }
- // Essentially a validation check on stim param specification after
- // Stimulator construction.
+
+ ///
+ /// A validation check on stimulation parameter specification after
+ /// Stimulator construction. True if:
+ /// 1) all specified default/fixed values are within the specified data
+ /// limits per specified parameter, and
+ /// 2) all param keys marked as modulatable are real specified parameters
+ ///
public bool ValidStimParamSpecification =>
- // a) at least the base stim params are included in the enum.
- BaseStimParams.ParamOrderIndices.Keys.All(
- this.StimParamData.Keys.Contains) &&
- // b) all available parameters are from the same enum
- this.StimParamData.Keys.Select(param => param.GetType())
- .Distinct().Count() == 1;
-
- // Instantiated in constructor based on the keys in StimParamData.
+ // 1) Spec provides value default/fixed values
+ StimParamSpecs.All(
+ kvp => kvp.Value.Item1.IsValidDataValue(kvp.Value.Item2)) &&
+ // 2) All param keys in modulatable list are specified
+ ModulatableStimParams.All(k => StimParamSpecs.Keys.Contains(k));
+
+ // Instantiated in constructor based on the keys in StimParamSpecs.
protected readonly SortedSet _StimParamsAvailable;
public SortedSet StimParamsAvailable => this._StimParamsAvailable;
//new SortedSet((Enum[])Enum.GetValues(typeof(StimParamType)));
@@ -96,7 +121,7 @@ protected Stimulator()//, int baseOutputConfigId)
this.Id = -1;
// Store the param options.
- this._StimParamsAvailable = new(this.StimParamData.Keys);
+ this._StimParamsAvailable = new(this.StimParamSpecs.Keys);
// Init empty output config dict and array of used markings per output.
this._outputConfigs = new();
@@ -116,14 +141,21 @@ protected Stimulator()//, int baseOutputConfigId)
///
public abstract bool IsValidOutputWiring(IEnumerable localOutputIds);
+ // TODO: use this somewhere!!! e.g., UpdateStim()
public bool IsValidParamValue(string stimParam, object paramValue)
{
- var (paramLims, defaultVal) = this.StimParamData[stimParam];
+ var (paramLims, defaultVal) = this.StimParamSpecs[stimParam];
return paramLims.IsValidDataValue(paramValue);
}
+
+ // TODO: calibration/comfort safety check function, then also call that
+ // somewhere like IsValidParamValue, e.g., in UpdateStim
+
+
// MAIN UPDATE STIM METHOD PASSED TO EACH THREAD DURING CONFIG AND CALLED
// BY TRANSDUCER ON UPDATE
+ // TODO; propogate exception
public bool UpdateStim(object state)
{
// TODO: there is probably a better way to do this global-local output
@@ -135,10 +167,13 @@ public bool UpdateStim(object state)
Dictionary))state;
var trainsParams = data.Item1;
var localOutputAssignments = data.Item2;
+ // TODO: apply check functions
return this.HW_UpdateStim(trainsParams, localOutputAssignments);
}
//protected abstract bool HW_UpdateStim(IEnumerable stimTrains,
// Dictionary globalToLocalOutputIds);
+ // TODO @Rachel: implement for WSS! NOT UpdateStim
+ // TODO: throw exception upon failure
protected abstract bool HW_UpdateStim(
IEnumerable> trainsParams,
Dictionary localOutputAssignments);
@@ -234,6 +269,4 @@ protected void SendMessage(byte[] data)
///
/// The full byte array of data to send.
protected abstract void HW_SendMessage(byte[] data);
-
-}
-
+}
\ No newline at end of file
diff --git a/src/EStimLibrary/Core/Stimulation/ThreadConfigDataPerStimulator.cs b/src/EStimLibrary/Core/Stimulation/ThreadConfigDataPerStimulator.cs
index b1308cd..d981b77 100644
--- a/src/EStimLibrary/Core/Stimulation/ThreadConfigDataPerStimulator.cs
+++ b/src/EStimLibrary/Core/Stimulation/ThreadConfigDataPerStimulator.cs
@@ -1,4 +1,5 @@
using EStimLibrary.Core.HardwareInterfaces;
+using EStimLibrary.Core.Data;
namespace EStimLibrary.Core.Stimulation;
@@ -6,7 +7,7 @@ namespace EStimLibrary.Core.Stimulation;
public record ThreadConfigDataPerStimulator(int GlobalStimId,
IEnumerable IndependentLeads,
- Dictionary> StimParamData,
+ Dictionary> StimParamSpecs,
// TODO: edit once have sorted how will resolve StimParams and enum stuff.
SortedSet ModulatableStimParams);
diff --git a/src/EStimLibrary/Core/Utils.cs b/src/EStimLibrary/Core/Utils.cs
index c01ebf5..4371f8c 100644
--- a/src/EStimLibrary/Core/Utils.cs
+++ b/src/EStimLibrary/Core/Utils.cs
@@ -2,7 +2,7 @@
using System.IO.Ports;
using System.Reflection;
-using EStimLibrary.Core.Haptics;
+using EStimLibrary.Core.Data;
using EStimLibrary.Core.Stimulation.Stimulators;
diff --git a/src/EStimLibrary/EStimLibrary.csproj b/src/EStimLibrary/EStimLibrary.csproj
index d7cd0ff..1831c35 100644
--- a/src/EStimLibrary/EStimLibrary.csproj
+++ b/src/EStimLibrary/EStimLibrary.csproj
@@ -1,7 +1,7 @@
- net7.0
+ net9.0enableenable
@@ -25,6 +25,8 @@
+
+
@@ -44,9 +46,11 @@
+
+
-
+
diff --git a/src/EStimLibrary/Extensions/Data/ContinuousDataLimits.cs b/src/EStimLibrary/Extensions/Data/ContinuousDataLimits.cs
new file mode 100644
index 0000000..908e030
--- /dev/null
+++ b/src/EStimLibrary/Extensions/Data/ContinuousDataLimits.cs
@@ -0,0 +1,30 @@
+using EStimLibrary.Core.Data;
+
+
+namespace EStimLibrary.Extensions.Data;
+
+
+///
+/// A limit structure for data that can have values within a continuous,
+/// real-valued range.
+///
+/// The lower bound, inclusive.
+/// The upper bound, inclusive.
+/// TODO: add resolution? and rounding scheme param? or leave rounding scheme
+/// for elsewhere to do since this is just validation, not subsequent action...
+public record ContinuousDataLimits(double MinBound, double MaxBound) :
+ IDataLimits
+{
+ public string Name => "Continuous Data Limits";
+ public Type ValidDataType => typeof(double);
+ public string Description => $"{this.Name}: [{this.MinBound}, " +
+ $"{this.MaxBound}]";
+
+ public bool IsValidDataValue(object value)
+ {
+ return value is not null &&
+ value.GetType() == this.ValidDataType &&
+ this.MinBound <= (double)value &&
+ (double)value <= this.MaxBound;
+ }
+}
\ No newline at end of file
diff --git a/src/EStimLibrary/Extensions/Data/ContinuousIntDataLimits.cs b/src/EStimLibrary/Extensions/Data/ContinuousIntDataLimits.cs
new file mode 100644
index 0000000..d4565a0
--- /dev/null
+++ b/src/EStimLibrary/Extensions/Data/ContinuousIntDataLimits.cs
@@ -0,0 +1,28 @@
+using EStimLibrary.Core.Data;
+
+
+namespace EStimLibrary.Extensions.Data;
+
+
+///
+/// A limit structure for data that can have values within a continuous,
+/// integer-valued range.
+///
+/// The lower bound, inclusive.
+/// The upper bound, inclusive.
+public record ContinuousIntDataLimits(int MinBound, int MaxBound) :
+ IDataLimits
+{
+ public string Name => "Continuous Integer Data Limits";
+ public Type ValidDataType => typeof(int);
+ public string Description => $"{this.Name}: [{this.MinBound}, " +
+ $"{this.MaxBound}]";
+
+ public bool IsValidDataValue(object value)
+ {
+ return value is not null &&
+ value.GetType() == this.ValidDataType &&
+ this.MinBound <= (int)value &&
+ (int)value <= this.MaxBound;
+ }
+}
\ No newline at end of file
diff --git a/src/EStimLibrary/Extensions/Data/DynamicDataLimits.cs b/src/EStimLibrary/Extensions/Data/DynamicDataLimits.cs
new file mode 100644
index 0000000..05e84b1
--- /dev/null
+++ b/src/EStimLibrary/Extensions/Data/DynamicDataLimits.cs
@@ -0,0 +1,31 @@
+using EStimLibrary.Core.Data;
+
+
+namespace EStimLibrary.Extensions.Data;
+
+
+///
+/// A limit structure for data with run-time-dependent limits or some other form
+/// of dynamic validation.
+///
+/// The data type of the associated data.
+/// The dynamic validation function that is called
+/// within the IsValidDataValue() method of this limits record. The function
+/// must take in the data value (of the correct data type) and return a boolean
+/// indicating if the data value is valid at the time when the function is
+/// called.
+/// The string description of the dynamic validation.
+/// Will be used as the overall description for the limits imposed.
+public record DynamicDataLimits(
+ Func CheckFunction, string Description) : IDataLimits
+{
+ public string Name => "Dynamic Data Limits";
+ public Type ValidDataType => typeof(DataType);
+
+ public bool IsValidDataValue(object value)
+ {
+ return value is not null &&
+ value.GetType() == this.ValidDataType &&
+ this.CheckFunction((DataType)value);
+ }
+}
\ No newline at end of file
diff --git a/src/EStimLibrary/Extensions/Data/FixedOptionDataLimits.cs b/src/EStimLibrary/Extensions/Data/FixedOptionDataLimits.cs
new file mode 100644
index 0000000..c17c9ce
--- /dev/null
+++ b/src/EStimLibrary/Extensions/Data/FixedOptionDataLimits.cs
@@ -0,0 +1,29 @@
+using EStimLibrary.Core.Data;
+
+
+namespace EStimLibrary.Extensions.Data;
+
+
+///
+/// A limit structure for data that can only have values from a specific list.
+///
+/// The data type of the specific options for the
+/// associated data.
+/// The specific values the associated data can be.
+///
+public record FixedOptionDataLimits(
+ SortedSet DataOptions) : IDataLimits
+{
+ public string Name => "Fixed Option Data Limits";
+ public Type ValidDataType => typeof(DataType);
+ public string Description => $"Data of type '{typeof(DataType)}' with " +
+ $"discrete options:\n\t" +
+ string.Join($"\n\t", this.DataOptions);
+
+ public bool IsValidDataValue(object value)
+ {
+ return value is not null &&
+ value.GetType() == this.ValidDataType &&
+ this.DataOptions.Contains((DataType)value);
+ }
+}
\ No newline at end of file
diff --git a/src/EStimLibrary/Extensions/Data/SequenceDataLimits.cs b/src/EStimLibrary/Extensions/Data/SequenceDataLimits.cs
new file mode 100644
index 0000000..a6f6f6e
--- /dev/null
+++ b/src/EStimLibrary/Extensions/Data/SequenceDataLimits.cs
@@ -0,0 +1,118 @@
+using EStimLibrary.Core.Data;
+
+
+namespace EStimLibrary.Extensions.Data;
+
+
+///
+/// A limit structure for data that is itself a sequence of data values, each
+/// element with its own subsequent data limits.
+///
+/// A list of string names for each element in
+/// the sequence. The names must be unique and in the desired order.
+/// The corresponding data limits for each element,
+/// keyed by the element's string name which must be present in
+/// OrderedElementNames.
+/// An optional parameter: the string names of
+/// any elements in the sequence that are themselves optional and don't have to
+/// be included in an actual sequence data record. This set must only contain
+/// strings found in OrderedElementNames. Value is null by default, i.e., if all
+/// elements are required.
+public record SequenceDataLimits(List OrderedElementNames,
+ Dictionary ElementLimits,
+ IEnumerable OptionalElements = null) : IDataLimits
+{
+ public string Name => "Sequence Data Limits";
+ // At bare minimum, data instances valid for further validation of this
+ // against this sequence limitations are ordered lists of sequence element
+ // name-value pairs.
+ public Type ValidDataType => typeof(List>);
+ public string Description
+ {
+ get
+ {
+ string description = "A sequence of data values, each with their " +
+ $"own data limits, as follows:\n";
+ // Add the description of each element's data limits.
+ for (int i = 0; i < this.OrderedElementNames.Count; i++)
+ {
+ string elementName = this.OrderedElementNames[i];
+
+ // Add a '*' to the beginning of the element description if it
+ // is an optional element in the sequence.
+ bool isOptional = this.OptionalElements is not null && this.OptionalElements.Contains(elementName);
+ string optionalPrefix = isOptional ? "*" : "";
+
+ description = description + $"\t{optionalPrefix}" +
+ $"Element {i + 1} = '{elementName}': " +
+ $"{this.ElementLimits[elementName].Description}\n";
+ }
+ return description;
+ }
+ }
+
+ public bool IsValidDataValue(object value)
+ {
+ // Fail immediately if null or not a list of name-value pairs is given.
+ if (value is null || value.GetType() != this.ValidDataType)
+ {
+ return false;
+ }
+
+ // Extract the sequence, a list of element name-value pairs.
+ var sequence = (List>)value;
+
+ // Iterate through each element name-value pair.
+ int headerIndex = 0;
+ for (int dataIndex = 0; dataIndex < sequence.Count; dataIndex++)
+ {
+ // Return failure if data values provided but no more expected
+ // in the sequence.
+ if (headerIndex >= this.OrderedElementNames.Count)
+ {
+ return false;
+ }
+
+ // Get the element name-value pair.
+ (string givenName, object givenValue) = sequence[dataIndex];
+
+ // Get the name of the element expected to be at this index.
+ var expectedName = this.OrderedElementNames[headerIndex];
+
+ // Check if the expected and given names match.
+ if (!givenName.Equals(expectedName))
+ {
+ // If names don't match, check if the expected element is
+ // optional. Simply go to next header index if so.
+ if (this.OptionalElements is not null &&
+ this.OptionalElements.Contains(expectedName))
+ {
+ // Decrement the data index so this value is checked again
+ // and not skipped.
+ dataIndex--;
+ }
+ // Otherwise, return failure: the sequence is missing an
+ // element.
+ else
+ {
+ return false;
+ }
+ }
+
+ // Otherwise, the names match, so get the data limits.
+ var limits = this.ElementLimits[givenName];
+ // Return failure if an invalid element value is given.
+ if (!limits.IsValidDataValue(givenValue))
+ {
+ return false;
+ }
+
+ // Otherwise, it's valid data so far. Look at the next expected
+ // value.
+ headerIndex++;
+ }
+
+ // If made it to the end of the loop, all data valid. Return success.
+ return true;
+ }
+}
\ No newline at end of file
diff --git a/src/EStimLibrary/Extensions/Data/StringDataLimits.cs b/src/EStimLibrary/Extensions/Data/StringDataLimits.cs
new file mode 100644
index 0000000..71396dd
--- /dev/null
+++ b/src/EStimLibrary/Extensions/Data/StringDataLimits.cs
@@ -0,0 +1,17 @@
+using EStimLibrary.Core.Data;
+
+
+namespace EStimLibrary.Extensions.Data;
+
+
+public record StringDataLimits : IDataLimits
+{
+ public string Name => "String Data Limits";
+
+ public Type ValidDataType => typeof(string);
+ public string Description => "Any string-type data.";
+ public bool IsValidDataValue(object value)
+ {
+ return value.GetType() == this.ValidDataType;
+ }
+}
\ No newline at end of file
diff --git a/src/EStimLibrary/Extensions/Haptics/ClassicDirectTransducer.cs b/src/EStimLibrary/Extensions/Haptics/ClassicDirectTransducer.cs
index 8709cbb..da1dde6 100644
--- a/src/EStimLibrary/Extensions/Haptics/ClassicDirectTransducer.cs
+++ b/src/EStimLibrary/Extensions/Haptics/ClassicDirectTransducer.cs
@@ -3,6 +3,7 @@
using EStimLibrary.Core.Haptics;
using EStimLibrary.Core.SpatialModel;
using EStimLibrary.Core;
+using EStimLibrary.Extensions.Data;
namespace EStimLibrary.Extensions.Haptics;
@@ -19,15 +20,11 @@ public class ClassicDirectTransducer : HapticTransducer
public ClassicDirectTransducer(string modParam)
{
// TODO: adjust this validation to be flexible to user-input param
- // lists.
+ // lists? or just relegate validation to session config??
// TODO: how to also factor in stimulator-specific modulation abilities?
// e.g., even if valid param name, the stimulator used for a given event
// may not be able to mod it...
- if (!BaseStimParams.ParamOrderIndices.Keys.Contains(modParam))
- {
- throw new ArgumentException($"{this.Name} Constructor Error: " +
- $"{modParam} is not a valid stim param.");
- }
+
this.ModulatedParam = modParam;
}
@@ -59,8 +56,7 @@ protected override IEnumerable _TransduceHapticEvent(
// i.e., P not included at all (otherwise would be first), use the first
// value.
// Should be between 0 and 1
- // TODO: make sure array len >0 before indexing into
- double modValue = hapticEvent.HapticParamData[0];
+ double modValue = hapticEvent.HapticParamData[HapticParam.P];
// StimThread properties:
// PerStimulatorConfigs (MAIN constructor input; next 2 derive from it)
diff --git a/src/EStimLibrary/Extensions/HardwareInterfaces/ContactGroup.cs b/src/EStimLibrary/Extensions/HardwareInterfaces/ContactGroup.cs
index 876b8d8..afe8df1 100644
--- a/src/EStimLibrary/Extensions/HardwareInterfaces/ContactGroup.cs
+++ b/src/EStimLibrary/Extensions/HardwareInterfaces/ContactGroup.cs
@@ -4,16 +4,38 @@
namespace EStimLibrary.Extensions.HardwareInterfaces;
+///
+/// A generic neural interface containing 1+ contacts.
+///
public class ContactGroup : NeuralInterfaceHardware
{
+ ///
+ /// The name of this neural interface type.
+ ///
public override string Name => "Contact Group";
+ ///
+ /// The integer number of contacts in this neural interface.
+ ///
public override int NumContacts => this._NumContacts;
protected int _NumContacts;
+ ///
+ /// Create a contact group with a positive integer number of contacts.
+ ///
+ /// Number of contacts.
+ /// Invalid argument, numContact must be
+ /// positive.
public ContactGroup(int numContacts) : base()
{
- this._NumContacts = numContacts;
+ if (numContacts > 0)
+ {
+ this._NumContacts = numContacts;
+ }
+ else
+ {
+ throw new ArgumentException("Number of contacts must be positive");
+ }
}
}
diff --git a/src/EStimLibrary/Extensions/HardwareInterfaces/GelPad.cs b/src/EStimLibrary/Extensions/HardwareInterfaces/GelPad.cs
index 28ad9fe..cafbf7f 100644
--- a/src/EStimLibrary/Extensions/HardwareInterfaces/GelPad.cs
+++ b/src/EStimLibrary/Extensions/HardwareInterfaces/GelPad.cs
@@ -4,12 +4,24 @@
namespace EStimLibrary.Extensions.HardwareInterfaces;
+///
+/// A single gel pad electrode.
+///
public class GelPad : NeuralInterfaceHardware
{
+ ///
+ /// The name of this neural interface type.
+ ///
public override string Name => "Gel Pad";
+ ///
+ /// A gel pad represents a single contact.
+ ///
public override int NumContacts => 1;
+ ///
+ /// Create a gel pad.
+ ///
public GelPad() : base()
{
}
diff --git a/src/EStimLibrary/Extensions/SpatialModel/StringHierarchy/StringHierarchyAreaFactory.cs b/src/EStimLibrary/Extensions/SpatialModel/StringHierarchy/StringHierarchyAreaFactory.cs
index f2bedd2..3a4582c 100644
--- a/src/EStimLibrary/Extensions/SpatialModel/StringHierarchy/StringHierarchyAreaFactory.cs
+++ b/src/EStimLibrary/Extensions/SpatialModel/StringHierarchy/StringHierarchyAreaFactory.cs
@@ -1,4 +1,6 @@
using EStimLibrary.Core;
+using EStimLibrary.Core.Data;
+using EStimLibrary.Extensions.Data;
using EStimLibrary.Core.SpatialModel;
diff --git a/src/EStimLibrary/Extensions/SpatialModel/StringHierarchy/StringHierarchyBodyModel.cs b/src/EStimLibrary/Extensions/SpatialModel/StringHierarchy/StringHierarchyBodyModel.cs
index 45b872d..3436dab 100644
--- a/src/EStimLibrary/Extensions/SpatialModel/StringHierarchy/StringHierarchyBodyModel.cs
+++ b/src/EStimLibrary/Extensions/SpatialModel/StringHierarchy/StringHierarchyBodyModel.cs
@@ -1,4 +1,5 @@
using EStimLibrary.Core;
+using EStimLibrary.Core.Data;
using EStimLibrary.Core.SpatialModel;
diff --git a/src/EStimLibrary/Extensions/SpatialModel/StringHierarchy/StringHierarchyLocationFactory.cs b/src/EStimLibrary/Extensions/SpatialModel/StringHierarchy/StringHierarchyLocationFactory.cs
index b07b0b1..3d04e62 100644
--- a/src/EStimLibrary/Extensions/SpatialModel/StringHierarchy/StringHierarchyLocationFactory.cs
+++ b/src/EStimLibrary/Extensions/SpatialModel/StringHierarchy/StringHierarchyLocationFactory.cs
@@ -1,4 +1,6 @@
using EStimLibrary.Core;
+using EStimLibrary.Core.Data;
+using EStimLibrary.Extensions.Data;
using EStimLibrary.Core.SpatialModel;
diff --git a/src/EStimLibrary/Extensions/SpatialModel/StringHierarchy/StringHierarchyRegion.cs b/src/EStimLibrary/Extensions/SpatialModel/StringHierarchy/StringHierarchyRegion.cs
index fe56217..d8523f1 100644
--- a/src/EStimLibrary/Extensions/SpatialModel/StringHierarchy/StringHierarchyRegion.cs
+++ b/src/EStimLibrary/Extensions/SpatialModel/StringHierarchy/StringHierarchyRegion.cs
@@ -11,21 +11,31 @@ public class StringHierarchyRegion
/// The base name of this region, e.g., hand.
///
public string BaseName { get; init; }
+
///
- /// The parent region of this region.
+ /// The parent region of this region. Null if this region is the root
+ /// region.
///
- public StringHierarchyRegion ParentRegion { get; set; }
+ public StringHierarchyRegion? ParentRegion { get; set; }
+
///
- /// The all possible option strings through the parent tree to get to this
- /// region.
+ /// All region options specified through the path through the parent tree
+ /// to this region.
///
public HashSet ParentOptions { get; set; }
///
- /// Options of the base region that can be selected, e.g., [left, right].
+ /// Options of this base region that can be selected, e.g., [left, right].
/// Ensured to be non-null. Empty if none.
///
public HashSet Options { get; set; }
+ ///
+ /// Whether or not there are options that must be specified at this base
+ /// region.
+ ///
public bool HasOptions => this.Options.Count > 0;
+ ///
+ /// All optioned region names that can be specified at this base region.
+ ///
public List OptionedRegionNames
{
get
@@ -34,16 +44,18 @@ public List OptionedRegionNames
if (this.HasOptions)
{
return this.Options.Select(
- option => $"{option}{StringHierarchySpec.OPTION_REGION_DELIMITER}" +
- $"{this.BaseName}").ToList();
+ option => $"{option}" +
+ $"{StringHierarchySpec.OPTION_REGION_DELIMITER}" +
+ $"{this.BaseName}").ToList();
}
- // Else, add include the base name.
+ // Else, just include the base name.
else
{
return new() { this.BaseName };
}
}
}
+
///
/// Directional modifiers that could be applied to this region. Dictionary
/// keyed by directional axis name. Value set contains possible modifiers
@@ -58,9 +70,27 @@ public Dictionary> Modifiers
_UpdateFrequencyDict();
}
}
+ ///
+ /// Whether or not there are directional modifiers specified at this base
+ /// region.
+ ///
public bool HasModifiers => this.Modifiers.Count > 0;
- private Dictionary> _modifiers;
+ ///
+ /// Directional modifiers that could be applied to this region. Dictionary
+ /// keyed by directional axis name. Value set contains possible modifier
+ /// values along that axis. Ensured to be non-null. Empty if none. Accessed
+ /// by the Modifiers property.
+ ///
+ private Dictionary> _modifiers = new();
+ ///
+ /// Dictionary of how frequently a modifier value occurs across all
+ /// specified modifier axes. Keyed by modifier value. Integer value is how
+ /// many axes contain that modifier value. Used when parsing string
+ /// hierarchy modifier specs to handle axes with the same modifier value,
+ /// e.g., x: center, y: center. Updated by the Mofidiers property setter.
+ ///
private Dictionary _modifierFrequencyDict = new();
+
///
/// The subregions of this region, keyed by string name. Ensured to be
/// non-null. Empty if none, meaning this region is a "leaf" in the nodal
@@ -71,7 +101,14 @@ public Dictionary Subregions
get;
protected set;
}
+ ///
+ /// Whether or not this region has subregions.
+ ///
public bool HasSubregions => this.Subregions.Count > 0;
+ ///
+ /// Whether or not this region is a "leaf" in the nodal graph, i.e., has no
+ /// subregions.
+ ///
public bool IsLeaf => this.Subregions.Count == 0;
///
@@ -83,17 +120,41 @@ public Dictionary Subregions
///
public SortedSet SavedAreas { get; set; }
- // Deep copies made of data structs but NOT of any referenced
- // StringHierarchyRegions. References retained to those objects.
- public StringHierarchyRegion(string baseName, StringHierarchyRegion parent,
- HashSet parentOptions = null,
- HashSet options = null,
- Dictionary> modifiers = null,
- Dictionary subregions = null)
+ ///
+ /// Create a new StringHierarchyRegion with the given base name and parent
+ /// region. Optionally provide parent options, region options, modifiers,
+ /// and subregions. Ensures all properties non-null after construction.
+ /// Deep copies made of collections passed in but NOT of any referenced
+ /// StringHierarchyRegions. References retained to those objects.
+ ///
+ /// The base name of this region, e.g., "hand".
+ ///
+ /// The parent region of this region (default: null,
+ /// meaning root region).
+ /// The specification of option values passed
+ /// to this region by the parent region (default: empty).
+ /// The possible option values for this region
+ /// (default: empty)
+ /// The directional modifiers that can be specified
+ /// at this region (default: empty). Key by modifier axis name. Valued by
+ /// set of possible modifier values along that directional axis.
+ /// The subregions of this region, if any
+ /// (default: empty). If none, this region is a "leaf" in the nodal graph.
+ ///
+ public StringHierarchyRegion(string baseName,
+ StringHierarchyRegion? parent = null,
+ // Note: the ! alleviates null warning, promising the value will be set
+ // to not-null in the constructor.
+ HashSet parentOptions = null!,
+ HashSet options = null!,
+ Dictionary> modifiers = null!,
+ Dictionary subregions = null!)
{
+ // Store base name and reference to parent region.
this.BaseName = baseName;
this.ParentRegion = parent;
- // Deep copy options and modifier structs.
+
+ // Deep copy options and modifier collection structs.
this.ParentOptions = (parentOptions is not null) ? new(parentOptions) :
new();
this.Options = (options is not null) ? new(options) : new();
@@ -103,6 +164,8 @@ public StringHierarchyRegion(string baseName, StringHierarchyRegion parent,
// Copy the dictionary but keep references to same subregion objects.
this.Subregions = (subregions is not null) ? new(subregions) : new();
+ // Initialize sets noting the IDs of saved locations and areas that
+ // point to this region.
this.SavedLocations = new();
this.SavedAreas = new();
}
@@ -113,18 +176,22 @@ public StringHierarchyRegion(string baseName, StringHierarchyRegion parent,
/// any existing subregion. Sets the parent reference of the added subregion
/// to be this region.
///
- /// The subregion to add. Shallow copied.
- /// Parent reference set.
+ /// The subregion to add. Shallow copied. Parent
+ /// reference set.
/// An output parameter: the replaced but
- /// unaltered existing subregion if any, else null.
+ /// unaltered existing subregion of the same base name if any, else null.
+ ///
+ /// The provided subregion
+ /// argument is null.
public void AddSubregion(StringHierarchyRegion subregion,
out StringHierarchyRegion? existingSubregion)
{
- // Fill the out parameter with the existing subregion if exists.
+ // Fill the output parameter with the existing subregion if exists.
if (this.Subregions.TryGetValue(subregion.BaseName,
out existingSubregion))
{
- // Overwrite the stored subregion with this basename.
+ // Replace the existing subregion at this basename with the new
+ // one.
this.Subregions[subregion.BaseName] = subregion;
}
// Else add the new subregion keyed by its basename.
@@ -137,19 +204,19 @@ public void AddSubregion(StringHierarchyRegion subregion,
subregion.ParentRegion = this;
}
- // TODO: TEST THE HECK OUT OF THIS
///
/// Try to get a given subregion of this region.
///
- /// The specified region to search for, given as a
- /// string sequence of appropriately delimited option-region names.
+ /// The string specification of the region to
+ /// search for, given as a string sequence of appropriately delimited
+ /// option-region names.
/// An output parameter: the searched
/// subregion if found, null if not.
/// True if the subregion could be found, False if not.
public bool TryGetSubregion(string regionSpec,
- out StringHierarchyRegion foundSubregion)
+ out StringHierarchyRegion? foundSubregion)
{
- // Split full linked name into sequence of option+region names.
+ // Split full region spec into sequence of option+region names.
var regionSet = StringHierarchySpec.ParseRegionSpec(regionSpec);
// Navigate the nodal graph to find the region.
@@ -182,7 +249,7 @@ public bool TryGetSubregion(string regionSpec,
if (searchCurrentRegion)
{
// If search name found, look for next item in subregions.
- if (foundSubregion.OptionedRegionNames.Contains(
+ if (foundSubregion!.OptionedRegionNames.Contains(
optionedRegionName))
{
searchCurrentRegion = false;
@@ -194,33 +261,54 @@ public bool TryGetSubregion(string regionSpec,
return false;
}
}
- // Else try to find matching subregion.
+ // Else search subregion for the option+region name.
else
{
- // Fail if search region found in subregion, else search for next
- // item in that subregion's subregions.
- if (!(foundSubregion.Subregions.TryGetValue(searchBaseName,
- out foundSubregion) && (searchOption.Equals("") ||
- foundSubregion.Options.Contains(searchOption))))
+ // Check if search basename is in subregions.
+ // Sets foundSubregion to the subregion if found.
+ bool viableSubregion = foundSubregion!.Subregions.TryGetValue(
+ searchBaseName, out foundSubregion);
+
+ // Fail if search basename not found in subregions or search
+ // option exists and not found in subregion matching search
+ // basename.
+ if (!(viableSubregion &&
+ (searchOption.Equals("") ||
+ foundSubregion!.Options.Contains(searchOption))))
{
foundSubregion = null;
return false;
}
+
+ // Else, search continues in located matching subregion.
}
}
- // If made it here, subregion is found. Fill output param and return.
+ // If made it here, subregion is found. Output param already filled.
+ // Return success.
return true;
}
+ ///
+ /// Check if a given modifier specification is valid within this region.
+ /// Valid if:
+ /// - formatted (delimited) correctly
+ /// - all modifiers values are found in the possible value sets of the
+ /// modifier axes of this region
+ /// - each modifier axis is used at most once
+ ///
+ /// The modifier specification to check.
+ /// T/F if the spec is valid in this region.
public bool IsValidModifierSpec(string modifierSpec)
{
- // Split into modifier set.
+ // Parse modifier spec into a set of modifier values.
var modifierSet = StringHierarchySpec.ParseModifierSpec(modifierSpec);
// Sort modifiers by their frequency across axis option sets, ascending
// order so duplicate modifier values (e.g., "center" as a valid value
// on two axes) doesn't use the only axis another modifier value may be
- // valid for.
+ // valid for. I.e., duplicate modifier values "used" for the most
+ // restricted axis first.
+ // TODO: FIX THIS!!!
var sortedModifierSet = modifierSet.OrderBy(modifier =>
this._modifierFrequencyDict.ContainsKey(modifier) ?
this._modifierFrequencyDict[modifier] : 0)
@@ -256,6 +344,10 @@ public bool IsValidModifierSpec(string modifierSpec)
return true;
}
+ ///
+ /// Update the frequency dictionary of modifier values across all axes
+ /// based on current Modifiers.
+ ///
private void _UpdateFrequencyDict()
{
// Clear the frequency dictionary
@@ -278,7 +370,6 @@ private void _UpdateFrequencyDict()
}
}
- // TODO: TEST!!!
///
/// Create a deep copy of the whole subtree starting at this region.
///
@@ -291,8 +382,9 @@ public StringHierarchyRegion DeepCopy(bool retainParentReference = true)
{
var parentRegion = (retainParentReference) ? this.ParentRegion : null;
// Create new region. Inherently deep copies options and modifiers.
- var newRegion = new StringHierarchyRegion(this.BaseName, parentRegion,
- options: this.Options, modifiers: this.Modifiers);
+ var newRegion = new StringHierarchyRegion(this.BaseName, parent: parentRegion!,
+ parentOptions: this.ParentOptions, options: this.Options,
+ modifiers: this.Modifiers);
// Add deep copies of all subregions.
foreach (var (_, subregion) in this.Subregions)
@@ -306,13 +398,32 @@ public StringHierarchyRegion DeepCopy(bool retainParentReference = true)
return newRegion;
}
+ ///
+ /// Create a string representation of this region and all subregions.
+ /// Override default behavior to provide a more detailed string output.
+ ///
+ /// A string representation of this region.
public override string ToString()
{
- return s_BuildSpecOptionsString("", 0, this);
+ return s_BuildSpecOptionsString(this);
}
- private static string s_BuildSpecOptionsString(string parentRegionSpec,
- int indentLevel, StringHierarchyRegion region)
+ ///
+ /// Create a string representation of the given region, recursing to contain
+ /// all subregions.
+ ///
+ /// The region to build a string representation of.
+ ///
+ /// The string specification of the parent
+ /// region (default: empty string, assuming given region is a root).
+ ///
+ /// Depth in the tree and thus number of indents
+ /// to included in the string output (default: 0, assuming given region is
+ /// a root).
+ /// A printable string representation of the given region.
+ ///
+ private static string s_BuildSpecOptionsString(StringHierarchyRegion region,
+ string parentRegionSpec = "", int indentLevel = 0)
{
// Output: [prev regionSpec], [options] baseName | [mod1Options], ...
@@ -347,8 +458,9 @@ private static string s_BuildSpecOptionsString(string parentRegionSpec,
List subregionStrings = new() { fullSpec };
foreach (var (_, subregion) in region.Subregions)
{
- subregionStrings.Add(s_BuildSpecOptionsString(regionSpec,
- indentLevel + 1, subregion));
+ subregionStrings.Add(s_BuildSpecOptionsString(subregion,
+ parentRegionSpec: regionSpec,
+ indentLevel: indentLevel + 1));
}
// Return the single string.
diff --git a/src/EStimLibrary/Extensions/Stimulation/Stimulators/EchoStimulator.cs b/src/EStimLibrary/Extensions/Stimulation/Stimulators/EchoStimulator.cs
index 2c762ca..7347861 100644
--- a/src/EStimLibrary/Extensions/Stimulation/Stimulators/EchoStimulator.cs
+++ b/src/EStimLibrary/Extensions/Stimulation/Stimulators/EchoStimulator.cs
@@ -1,10 +1,8 @@
using EStimLibrary.Core.Stimulation.Stimulators;
-using EStimLibrary.Core.Stimulation.Data;
using EStimLibrary.Core;
using EStimLibrary.Core.Stimulation.Functions;
-using EStimLibrary.Core.Stimulation.Trains;
-using EStimLibrary.Extensions.Stimulation.Phases;
-using Newtonsoft.Json.Linq;
+using EStimLibrary.Core.Data;
+using EStimLibrary.Extensions.Data;
namespace EStimLibrary.Extensions.Stimulation.Stimulators;
@@ -28,14 +26,77 @@ public EchoStimulator(string port, int numOutputs)
// Only allow half as many configs as there are outputs.
public override int MaxNumOutputConfigs => this._NumOutputs / 2;
- // Use example limits and defaults for all base stim params.
+ #region Convenience StimParam String Name Defines
+ protected const string PA = "PA";
+ protected const string PW = "PW";
+ protected const string StimPhaseShape = "StimPhaseShape";
+ protected const string RechargePhaseShape = "RechargePhaseShape";
+ protected const string IPD = "IPD";
+ protected const string AnodeRatio = "AnodeRatio";
+ protected const string AnodeFirst = "AnodeFirst";
+ protected const string Period = "Period";
+ #endregion
+
+ ///
+ /// Example stimulation parameter specification.
+ /// {"paramNameOrKey": (dataLimitsObject, defaultOrFixedValue), ...}
+ ///
public override Dictionary>
- StimParamData => BaseStimParams.ExampleParamData;
- // Params that can be dynamically modulated
+ StimParamSpecs => new()
+ {
+ // Phase amplitude in mA
+ { PA, new(
+ new ContinuousDataLimits(0.0, 10.0),
+ 0.0)},
+ // Phase width in us
+ { PW, new(
+ new ContinuousDataLimits(0.0, 250.0),
+ 0.0)},
+ // Activation/stimulation phase shape
+ { StimPhaseShape, new(
+ new FixedOptionDataLimits(new()
+ {
+ "square",
+ "sine",
+ "triangle"
+ }),
+ "sine")},
+ // Recharge phase shape
+ { RechargePhaseShape, new(
+ new FixedOptionDataLimits(new()
+ {
+ "square",
+ "sine",
+ "triangle"
+ }),
+ "sine")},
+ // Inter-phase delay in us
+ { IPD, new(
+ new ContinuousDataLimits(0.0, 150.0),
+ 100.0)},
+ { AnodeRatio, new(
+ new ContinuousDataLimits(0.0, 12.0),
+ 12.0)},
+ { AnodeFirst, new(
+ new FixedOptionDataLimits(new()
+ {
+ Constants.ANODE_FIRST,
+ Constants.ANODE_SECOND
+ }),
+ Constants.ANODE_SECOND)},
+ // Pulse period in s (1/Hz)
+ { Period, new(
+ new ContinuousDataLimits(0.0, 1/250.0),
+ 1/100.0)}
+ };
+
+ ///
+ /// Example set of specified parameters that can be dynamically modulated.
+ ///
public override SortedSet ModulatableStimParams => new()
{
- BaseStimParams.PA,
- BaseStimParams.PW
+ PA,
+ PW
};
#region TODO
diff --git a/tests/EStimLibrary.UnitTests/Core/Haptics/HapticEventTests.cs b/tests/EStimLibrary.UnitTests/Core/Haptics/HapticEventTests.cs
new file mode 100644
index 0000000..973857a
--- /dev/null
+++ b/tests/EStimLibrary.UnitTests/Core/Haptics/HapticEventTests.cs
@@ -0,0 +1,63 @@
+using EStimLibrary.Core.Haptics;
+using EStimLibrary.Core.SpatialModel;
+
+
+namespace EStimLibrary.UnitTests.Core.Haptics;
+
+
+public class HapticEventTests
+{
+ private readonly ITestOutputHelper _output;
+
+ // Test class constructor creates an output helper so can write console
+ // output.
+ public HapticEventTests(ITestOutputHelper testOutputHelper)
+ {
+ this._output = testOutputHelper;
+ }
+
+ // Test method naming convention: LibClassMethodName_ScenarioShouldExpectn
+
+ ///
+ /// Test record initialization.
+ ///
+ [Fact]
+ public void Constuctor_ShouldInit()
+ {
+ var timestamp = DateTime.Now;
+ var locations = new Dictionary>();
+ var areas = new Dictionary>();
+ var hapticParams = new Dictionary();
+ hapticParams.Add(HapticParam.P, 2.5);
+ hapticParams.Add(HapticParam.dP, 1.0);
+ var hapticEvent = new HapticEvent(timestamp, locations, areas, hapticParams);
+ Assert.NotNull(hapticEvent);
+ Assert.Equal(timestamp, hapticEvent.Timestamp);
+ Assert.Equal(locations, hapticEvent.Locations);
+ Assert.Equal(areas, hapticEvent.Areas);
+ Assert.Equal(hapticParams, hapticEvent.HapticParamData);
+ Assert.True(hapticEvent.LocalizeByArea);
+ }
+
+ ///
+ /// Test record copying.
+ ///
+ [Fact]
+ public void With_ShouldCopy()
+ {
+ var timestamp = DateTime.Now;
+ var locations = new Dictionary>();
+ var areas = new Dictionary>();
+ var hapticParams = new Dictionary();
+ hapticParams.Add(HapticParam.P, 2.5);
+ hapticParams.Add(HapticParam.dP, 1.0);
+ var hapticEvent1 = new HapticEvent(timestamp, locations, areas, hapticParams);
+ var hapticEvent2 = hapticEvent1 with { };
+ var hapticEvent3 = hapticEvent2 with { Timestamp = DateTime.Now };
+ var hapticEvent4 = hapticEvent3 with { Areas = new Dictionary>() };
+
+ Assert.Equal(hapticEvent1, hapticEvent2);
+ Assert.NotEqual(hapticEvent2, hapticEvent3);
+ Assert.NotEqual(hapticEvent3, hapticEvent4);
+ }
+}
\ No newline at end of file
diff --git a/tests/EStimLibrary.UnitTests/Core/HardwareInterfaces/LeadTests.cs b/tests/EStimLibrary.UnitTests/Core/HardwareInterfaces/LeadTests.cs
new file mode 100644
index 0000000..647ddc4
--- /dev/null
+++ b/tests/EStimLibrary.UnitTests/Core/HardwareInterfaces/LeadTests.cs
@@ -0,0 +1,1103 @@
+using EStimLibrary.Core;
+using EStimLibrary.Core.HardwareInterfaces;
+
+
+namespace EStimLibrary.UnitTests.Core.HardwareInterfaces;
+
+
+///
+/// A class for unit testing the Lead class
+///
+public class LeadTests
+{
+ ///
+ /// Test the parameterized Lead constructor with different data values.
+ ///
+ /// The set of global contact IDs the lead is
+ /// connected to. Assumed to be valid.
+ /// The set of global output IDs the lead is
+ /// connected to. Assumed to be valid.
+ /// The default direction of current on the
+ /// lead.
+ [Theory]
+ [MemberData(nameof(Constructor_TestData))]
+ public void Constructor_ShouldInitSortedSetsAndDirection(
+ SortedSet contactSet, SortedSet outputSet,
+ Constants.CurrentDirection currentDirection)
+ {
+ // Act
+ var lead = new Lead(contactSet, outputSet, currentDirection);
+
+ // Assert
+ Assert.Equal(contactSet, lead.ContactSet);
+ Assert.Equal(outputSet, lead.OutputSet);
+ Assert.Equal(currentDirection, lead.CurrentDirection);
+ }
+
+ ///
+ /// Test parameter data for the Lead constructor, following the form:
+ /// globalContactIdSet,
+ /// globalOutputIdSet,
+ /// defaultCurrentDirection
+ /// Assuming all global IDs are valid (existing non-negative integers) in
+ /// the session.
+ ///
+ public static IEnumerable