-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathBlockData.cs
More file actions
229 lines (205 loc) · 8.87 KB
/
Copy pathBlockData.cs
File metadata and controls
229 lines (205 loc) · 8.87 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Security.Cryptography;
using System.Text;
namespace ubco.ovilab.uxf.extensions
{
/// <summary>
/// Object to parse json data from experiment server. An extension
/// of this class can be used as the generic type in <see
/// cref="ExperimentManager"/>. Data from experiment server would
/// automatically add the name and participant_index. The
/// calibrationName would need to be set in each block in the
/// experiment server config file.
/// </summary>
public class BlockData
{
private bool uniqueSeedComputed = false;
private int uniqueSeed;
/// <summary>
/// The name of the block.
/// </summary>
public string name;
/// <summary>
/// The participant index.
/// </summary>
public int participant_index;
/// <summary>
/// The calibration name.
/// </summary>
public string calibrationName;
/// <summary>
/// The block index in the list of blocks configured in
/// experiment-server.
/// </summary>
public int block_id;
public override string ToString()
{
StringBuilder sb = new StringBuilder($"Name: {name}");
sb.AppendLine($"Participant Index: {participant_index}");
sb.AppendLine($"Calibration name: {calibrationName}");
sb.AppendLine($"Block ID: {block_id}");
foreach (FieldInfo field in GetFields(this.GetType()))
{
sb.AppendLine($"{field.Name}: {field.GetValue(this)}");
}
return sb.ToString();
}
// KLUDGE: This is not used often, maybe cache if performance
// is too much of an issue here?
protected static FieldInfo[] GetFields(Type type)
{
return type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.FlattenHierarchy);
}
/// <summary>
/// Returns all the fields as a list of strings.
/// Can be used with <see cref="UXF.Session.settingsToLog"/>.
/// </summary>
public static List<string> GetFieldsAsSettingsToLog<T>() where T : BlockData
{
return GetFields(typeof(T)).Select(f => f.Name).ToList();
}
/// <summary>
/// Return a dictionary which can be used to add all
/// parameters to settings with:
/// <see cref="UXF.Settings.UpdateWithDict"/>
/// </summary>
public virtual Dictionary<string, object> GetDictionary()
{
Dictionary<string, object> dict = new();
FieldInfo[] fields = GetFields(this.GetType());
foreach (FieldInfo field in fields)
{
dict.Add(field.Name, field.GetValue(this));
}
return dict;
}
/// <summary>
/// Computes a deterministic 32-bit seed that uniquely
/// identifies the condition represented by the public
/// instance fields of this BlockData instance. The method
/// builds a stable, canonical byte representation of each
/// public instance field (fields are taken from GetType()
/// with BindingFlags.Instance | BindingFlags.Public |
/// BindingFlags.FlattenHierarchy and sorted by field name),
/// hashes that representation with SHA-256, and folds part of
/// the hash into a 32-bit signed integer.
/// </summary>
/// <returns>
/// A deterministic int derived from the SHA-256 hash of the
/// canonical field representation. The value may be
/// negative. The same set of field names and values (with
/// identical string/ToString() outputs and numeric encodings)
/// will always produce the same seed.
/// </returns>
/// <remarks>
/// This is a GPT generated function.
/// The computed seed is cached after the first call and
/// returned on subsequent calls. The initial computation is
/// thread-safe (uses double-checked locking). The canonical
/// representation encodes field names and values using UTF-8
/// for strings, fixed-size little-endian byte forms for
/// numeric types, a single marker for nulls, and a
/// type-qualified ToString() fallback for complex types. If
/// you override this method, preserve determinism if the
/// result is intended to uniquely identify experimental
/// conditions. This method is not intended for cryptographic
/// use; it is intended only to generate a stable condition
/// identifier from the current public instance fields.
/// </remarks>
public virtual int UniqueConditionSeed()
{
if (uniqueSeedComputed)
{
return uniqueSeed;
}
lock (this)
{
if (uniqueSeedComputed)
{
return uniqueSeed;
}
FieldInfo[] fields = GetFields(this.GetType());
Array.Sort(fields, (a, b) => string.Compare(a.Name, b.Name, StringComparison.Ordinal));
using (SHA256 sha = SHA256.Create())
using (MemoryStream ms = new MemoryStream())
{
Encoding enc = Encoding.UTF8;
foreach (FieldInfo field in fields)
{
object value;
try
{
value = field.GetValue(this);
}
catch
{
value = null;
}
// Field name to make representation stable
byte[] nameBytes = enc.GetBytes(field.Name);
ms.Write(nameBytes, 0, nameBytes.Length);
ms.WriteByte((byte)':');
if (value == null)
{
ms.WriteByte((byte)'N');
ms.WriteByte((byte)'\n');
continue;
}
Type t = value.GetType();
if (t == typeof(string))
{
String s = (string)value;
byte[] sb = enc.GetBytes(s);
ms.Write(sb, 0, sb.Length);
}
else if (t == typeof(float))
{
ms.Write(BitConverter.GetBytes((float)value), 0, 4);
}
else if (t == typeof(double))
{
ms.Write(BitConverter.GetBytes((double)value), 0, 8);
}
else if (t == typeof(decimal))
{
foreach (int part in decimal.GetBits((decimal)value))
{
ms.Write(BitConverter.GetBytes(part), 0, 4);
}
}
else if (t == typeof(bool))
{
ms.WriteByte((byte)(((bool)value) ? 1 : 0));
}
else if (t.IsPrimitive)
{
// canonicalize primitive numeric types by their 64-bit representation
long numeric = Convert.ToInt64(value);
ms.Write(BitConverter.GetBytes(numeric), 0, 8);
}
else
{
// fallback: include type and ToString to keep determinism
String s2 = value.GetType().FullName + ":" + value.ToString();
byte[] bs = enc.GetBytes(s2);
ms.Write(bs, 0, bs.Length);
}
ms.WriteByte((byte)'\n');
}
byte[] hash = sha.ComputeHash(ms.ToArray());
// Combine hash bytes into a 32-bit value. This is explicit and endianness-stable:
uint part0 = (uint)hash[0] | (uint)hash[1] << 8 | (uint)hash[2] << 16 | (uint)hash[3] << 24;
uint part1 = (uint)hash[4] | (uint)hash[5] << 8 | (uint)hash[6] << 16 | (uint)hash[7] << 24;
uint combined = part0 ^ part1; // fold entropy from two words
uniqueSeed = unchecked((int)combined); // allow negative seeds too
uniqueSeedComputed = true;
return uniqueSeed;
}
}
}
}
}