Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion doc/components/fixed_point.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ Fixed-point binary representation of numbers is useful several applications incl

## FixedPointValue

A [FixedPointValue](https://intel.github.io/rohd-hcl/rohd_hcl/FixedPointValue-class.html) represents a signed or unsigned fixed-point value following the Q notation (Qm.n format) as introduced by [Texas Instruments](https://www.ti.com/lit/ug/spru565b/spru565b.pdf). It comprises an optional sign, integer part and/or a fractional part. [FixedPointValue](https://intel.github.io/rohd-hcl/rohd_hcl/FixedPointValue-class.html)s can be constructed from individual fields or from a Dart [double](https://api.dart.dev/stable/3.6.0/dart-core/double-class.html), converted to Dart [double](https://api.dart.dev/stable/3.6.0/dart-core/double-class.html), can be compared and can be operated on (+, -, *, /). A `FixedPointValuePopulator` can be used to construct `FixedPointValue` using different kinds of converters.
A [FixedPointValue](https://intel.github.io/rohd-hcl/rohd_hcl/FixedPointValue-class.html) represents a signed or unsigned fixed-point value following the Q notation (Qm.n format) as introduced by [Texas Instruments](https://www.ti.com/lit/ug/spru565b/spru565b.pdf). It comprises an optional sign, integer part and/or a fractional part. [FixedPointValue](https://intel.github.io/rohd-hcl/rohd_hcl/FixedPointValue-class.html)s can be constructed from individual fields or from a Dart [double](https://api.dart.dev/stable/3.6.0/dart-core/double-class.html), converted to Dart [double](https://api.dart.dev/stable/3.6.0/dart-core/double-class.html), can be compared and can be operated on (+, -, *, /). [FixedPointValue] call toFloat method to switch from a fixed
point value to a floating point value representation. This will convert to the minimal floating point representation for what is the fixed point value to maintain accuracy.

## FixedPoint

Expand Down
78 changes: 78 additions & 0 deletions lib/src/arithmetic/values/fixed_point_value.dart
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,84 @@ class FixedPointValue implements Comparable<FixedPointValue> {
return isNegative() ? -value : value;
}

/// Converts a [FixedPointValue] to a [FloatingPointValue].
FloatingPointValue toFloatingPointValue({required bool explicitJBit}) {
if (!value.isValid) {
throw RohdHclException('Inputs must be valid.');
}

// throw an error if widths are both zero.
if (integerWidth == 0 && fractionWidth == 0) {
throw RohdHclException('Cannot convert fixed-point value with zero '
'widths to floating-point value.');
}

// Qm.n need to be converted so we have it in the form (1.<MANTISSA>).
const minmialExponentWidth = 4;
Comment thread
desmonddak marked this conversation as resolved.
var sign = value[-1];
if (!signed) {
sign = LogicValue.zero; // if not signed, we set sign to zero.
}
// need to verify if we have a signed representation or not.
final fixedNum = sign.toBool() ? ~value + 1 : value;

var firstOneIndex = 0;
final LogicValue mantissaVal;
final LogicValue exponentVal;

// if we have a zero value, we can return a zero floating point value.
if (fixedNum.isZero) {
return FloatingPointValue(
sign: LogicValue.zero,
exponent: LogicValue.zero,
mantissa: LogicValue.zero,
explicitjBit: true);
}

// Count the leading zeros in order to correct the exponent value.
for (var i = fixedNum.width - 1; i > 0; i--) {
if (fixedNum[i] == LogicValue.one) {
firstOneIndex = i;
break;
}
}
mantissaVal = explicitJBit
? fixedNum
.slice(firstOneIndex, 0)
.extend(firstOneIndex + 1, LogicValue.zero)
: firstOneIndex == 0
? LogicValue.filled(integerWidth + fractionWidth, LogicValue.zero)
: fixedNum.slice(firstOneIndex - 1, 0);

// now we have (1.<MANTISSA>) so we can calculate the exponent.
final radix = (fixedNum.width - 1) - integerWidth;
var shiftAmnt = firstOneIndex - radix;
if (!signed) {
shiftAmnt -= 1; // shiftAmnt is reduced by one for unsigned.
}
if (explicitJBit && mantissaVal[mantissaVal.width - 1] == LogicValue.zero) {
shiftAmnt += 1;
}
var expWidth = 0;
if (shiftAmnt != 0) {
// if shiftAmnt is not zero, we need to calculate the exponent width.
expWidth = log2Ceil(shiftAmnt.abs()) << 1;
}
if (expWidth < minmialExponentWidth) {
expWidth = minmialExponentWidth; // minimum exponent width needed.
}
// set the bias amount which is 2^(expWidth - 1) - 1.
final bias = LogicValue.ofInt((pow(2, expWidth - 1) - 1).toInt(), expWidth);
// bias our exponent value.
exponentVal = LogicValue.ofInt(shiftAmnt, expWidth) + bias;

return FloatingPointValue(
sign: sign,
exponent: exponentVal,
mantissa: mantissaVal,
explicitjBit: explicitJBit);
}

/// Addition operation that returns a [FixedPointValue].
Comment thread
mkorbel1 marked this conversation as resolved.
/// The result is signed if one of the operands is signed.
/// The result integer has the max integer width of the operands plus one.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -459,4 +459,35 @@ class FloatingPointValue implements Comparable<FloatingPointValue> {
sign.bitString, exponent.bitString, '${'0' * (mantissa.width - 1)}1');
}
}

/// Losslessly convert a [FloatingPointValue] to a [FixedPointValue].
FixedPointValue toFixedPointValue() {
// check for 'special value'
if (isNaN) {
throw RohdHclException('FloatingPointValue: Not a Number');
}
if (isAnInfinity) {
throw RohdHclException('FloatingPointValue: Infinity');
}
// space for full shift (bias + mantissa + sign)
final shift = exponent.toInt() - bias;

var fxdMantissa = [
if (isNormal()) LogicValue.one else LogicValue.zero,
mantissa
].swizzle().zeroExtend(shift.abs() + mantissaWidth + 3);

fxdMantissa = sign == LogicValue.one ? ~fxdMantissa + 1 : fxdMantissa;

// convert mantissa into 'value'
final shiftedFxdMantissa =
shift.isNegative ? fxdMantissa : fxdMantissa << shift;

final fxpN = shift.isNegative ? mantissaWidth - shift : mantissaWidth;
final fxpM = shiftedFxdMantissa.width - fxpN - 1;

return FixedPointValue.populator(
integerWidth: fxpM, fractionWidth: fxpN, signed: true)
.ofLogicValue(shiftedFxdMantissa);
}
}
32 changes: 32 additions & 0 deletions test/arithmetic/values/fixed_point_value_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -291,4 +291,36 @@ void main() {
}
}
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what about testing with vs without explicitJBit?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you mind expanding on that? Idk what that is but happy to add it to my tests.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://github.com/intel/rohd-hcl/blob/main/doc/components/floating_point.md#explicit-j-bit

I'll test the documentation on you: if this doc doesn't explain it well enough, we should improve it!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great question! I think maybe this would be an excellent second issue I can follow-up on in a different PR maybe? Honest answer is, even with reading that I'd need time to digest that and maybe that should be an expanded method to this base one? Or maybe I'm over complicating that.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it should be a relatively straight-forward change, basically even normal things look like sub-normals with an explicit 1. @desmonddak what do you think?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for the late reply: it should be quite simple; a small change in how you adjust the mantissa in each direction.
So going from fixed value to normal float value: you would traditionally compute the mantissa for the floating point value and chop off the leading '1' and store the rest of the mantissa into the FPV.

But going to an explicit j-bit form, you would keep that leading 1 and put it into the mantissa. The FPV feels effectively one bit shorter because you need to store the leading '1. Note that you would do this just as you do with subnormals. In other words, if you fixed point was small enough to require a subnormal representation in FPV, you would also keep that leading '1'.

Going from an explicitJBit normal float to fixed, you would recognize that you don't need to prefix the mantissa with a '1' representing the j-bit. The leading '1' is already in the mantissa. This is just like it is for subnormal. If you are converting a subnormal float to fixed, you do not prefix the mantissa with a '1'.

To test, you need to provide an explicitJBit form of floatingPointValue to translate to fixed and conversely, indicate that you want an explicitJBit form of floatingPointValue when converting from fixed.

Explicit JBIT is experimental: we recognize that we do not have to normalize in some situations as it doesn't produce any more accuracy (say adding two narrow mantissa FPs to get a wider mantissa FP). Rather than normalizing, we can leave it in explicit j-bit form for the next operation and save a normalization step, and let normalization happen after the next operation.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Something like this;
test('FixedPointValue: toFloatingPointValue signed', () {
const m = 10;
const n = 12;
const width = m + n + 1; // 1 for sign bit
for (var i = 0; i < pow(2, width); i++) {
final testVal = (i / pow(2, width)).toInt();
final fxv = FixedPointValue.populator(
integerWidth: m, fractionWidth: n, signed: true)
.ofLogicValue(LogicValue.ofInt(testVal, width));
FloatingPointValue fpv;
if (testVal > 0) {
fpv = fxv.toFloatingPointValue(false); // explicitJBit
} else {
fpv = fxv.toFloatingPointValue(true);
}
expect(fpv.toDouble(), fxv.toDouble(),
reason: 'toFloatingPointValue failed for $testVal');
}
});
?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jcfarwe has pushed tests for J-bit, ready for review @desmonddak and @mkorbel1

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tests look good. Can you merge the signed/unsigned versions into one?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you shorten the widths to speed up the tests? 22 bits is quite wide.

I tried the following which ran in about 1s:

    for (final (m, n) in [(5, 7), (7, 5), (0, 7), (7, 0)]) {

test('FixedPointValue: toFloatingPointValue signed', () {
for (final (m, n) in [(10, 12), (0, 22), (22, 0)]) {
final width = m + n + 1; // 1 for sign bit
for (final explicitJBit in [true, false]) {
for (var i = 0; i < pow(2, width); i++) {
final fxv = FixedPointValue.populator(
integerWidth: m, fractionWidth: n, signed: true)
.ofLogicValue(LogicValue.ofInt(i, width));
final fpv = fxv.toFloatingPointValue(explicitJBit: explicitJBit);
expect(fpv.toDouble(), fxv.toDouble(),
reason: 'toFloatingPointValue failed for $i');
}
}
}
});

test('FixedPointValue: toFloatingPointValue unsigned', () {
Comment thread
desmonddak marked this conversation as resolved.
for (final (m, n) in [(10, 12), (0, 22), (22, 0)]) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Definitely narrow these widths...

final width = m + n; // no sign bit for unsigned
for (final explicitJBit in [true, false]) {
for (var i = 0; i < pow(2, width); i++) {
final fxv =
FixedPointValue.populator(integerWidth: m, fractionWidth: n)
.ofLogicValue(LogicValue.ofInt(i, width));
final fpv = fxv.toFloatingPointValue(explicitJBit: explicitJBit);
expect(fpv.toDouble(), fxv.toDouble(),
reason: 'toFloatingPointValue failed for $i');
}
}
}
});
}
148 changes: 148 additions & 0 deletions test/arithmetic/values/floating_point_value_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -631,4 +631,152 @@ void main() {
}
});
});

@desmonddak desmonddak Aug 25, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We like to see a blank line between tests or groups.

group('FPV: toFixedPointValue', () {
// generate expected result of float2fixed conversion
FixedPointValue expectedResult(
int exp,
int expSize,
int sign,
int mant,
int mantSize,
LogicValue exponent,
) {
// generate expected result
final expAbs = exp.abs();
final shift =
expAbs + 3; // add two bits for integral part, one bit for sign

final mantissa = exponent != LogicValue.ofInt(0, expSize)
? LogicValue.ofInt(1 << mantSize | mant, mantSize + shift)
: LogicValue.ofInt(mant, mantSize + shift);
final shiftedMantissa = exp < 0 ? mantissa : mantissa << expAbs;
final finalMantissa = sign == 0 ? shiftedMantissa : ~shiftedMantissa + 1;

final nLen = exp.isNegative ? mantSize - exp : mantSize;
final mLen = finalMantissa.width - nLen - 1; // one bit for sign
return FixedPointValue.populator(
integerWidth: mLen,
fractionWidth: nLen,
signed: true,
).ofLogicValue(finalMantissa);
}

test('FPV: toFixedPointValue exhaustive', () async {

@desmonddak desmonddak Aug 25, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Something is fishy about this test. It only fills the upper 20 bits or so of the integer portion, yet the test is labeled 'exhaustive'.

Here is what I see when I print fxv:

(0 0010000000000000000101101100000000000000000000000000000000000000000000000000000000000000000000 00000000000000000000000)
(1 1101111111111111111010010100000000000000000000000000000000000000000000000000000000000000000000 00000000000000000000000)

// bit widths to be tested
// 5, 6, 7, 8-bit exponent
// 10, 12, 17, 23-bit mantissa
final expWidths = [5, 6, 7, 8];
final mantWidths = [10, 12, 17, 23];

for (final expWidth in expWidths) {
for (final mantWidth in mantWidths) {
final minExp = -1 << (expWidth - 1);
final maxExp = (1 << (expWidth - 1)) - 1;
for (var testExp = minExp; testExp < maxExp; testExp++) {
for (final sign in [LogicValue.zero, LogicValue.one]) {
final bias = (pow(2, expWidth - 1) - 1).toInt();
final exp = LogicValue.ofInt(testExp, expWidth);
final mant = LogicValue.ofInt(testExp, mantWidth);

final fpv1 = FloatingPointValue(
exponent: exp + bias,
sign: sign,
mantissa: mant,
);
if (fpv1.isNaN || fpv1.isAnInfinity) {
continue;
}

final fxv = fpv1.toFixedPointValue();

final expected = expectedResult(
testExp,
expWidth,
sign.toInt(),
mant.toInt(),
mantWidth,
fpv1.exponent,
);

expect(
fxv == expected,
true,
reason: 'Got $fxv expected $expected',
);
}
}
}
}
});
test('FPV: toFixedPointValue simplified', () async {
//
//(exp, expSize, sign (0 == +), mant, mantSize)
final testCases = [
(0, 8, 0, 0x000000, 23), // 1.0
(0, 8, 0, 0x000001, 23), // 1.0000001
(1, 8, 0, 0x000001, 23), // 2.0000002
(8, 8, 0, 0x000001, 23), // 256.00003
(0, 8, 0, 0x400000, 23), // 1.5
(-1, 8, 0, 0x400000, 23), // 0.75
(1, 8, 0, 0x400000, 23), // 3.0
(2, 8, 0, 0x400000, 23), // 6.0
(0, 8, 1, 0x000000, 23), // -1.0
(0, 8, 1, 0x400000, 23), // -1.5
(0, 5, 0, 0x000, 10), // 1.0, 16-bit float
(-127, 8, 0, 0x000001, 23), // 1e-45
(-127, 8, 0, 0x000000, 23), // 0
(-127, 8, 1, 0x000000, 23), // -0
];

for (final testCase in testCases) {
final exp = testCase.$1;
final expSize = testCase.$2;
final bias = (pow(2, expSize - 1) - 1).toInt();
final sign = testCase.$3;
final mant = testCase.$4;
final mantSize = testCase.$5;

final fpv1 = FloatingPointValue(
exponent: LogicValue.ofInt(exp + bias, expSize),
sign: LogicValue.ofInt(sign, 1),
mantissa: LogicValue.ofInt(mant, mantSize),
);
final fxv = fpv1.toFixedPointValue();

// generate expected result
final expected = expectedResult(
exp,
expSize,
sign,
mant,
mantSize,
fpv1.exponent,
);

expect(fxv == expected, true, reason: 'Got $fxv expected $expected');
}
});
});
test('FPV: toFixedPointValue, Special values', () async {
//
//[exp, expSize, sign (0 == +), mant, mantSize]
final testCases = [
[128, 8, 0, 0x400000, 23], // NaN
[128, 8, 1, 0x000000, 23], // -Inf
[128, 8, 0, 0x400000, 23], // Inf
];

for (final testCase in testCases) {
final bias = (pow(2, testCase[1] - 1) - 1).toInt();
final fpv1 = FloatingPointValue(
exponent: LogicValue.ofInt(testCase[0] + bias, testCase[1]),
sign: LogicValue.ofInt(testCase[2], 1),
mantissa: LogicValue.ofInt(testCase[3], testCase[4]));

expect(
fpv1.toFixedPointValue,
throwsA(isA<RohdHclException>()),
);
}
});
}