From 39efd629f1f8ef02762dd1a3d807ae8e66c16dd4 Mon Sep 17 00:00:00 2001 From: Oscar Gustafsson <oscar.gustafsson@gmail.com> Date: Wed, 17 May 2023 11:39:21 +0200 Subject: [PATCH] Fix typing, spelling errors and minor tests --- b_asic/codegen/vhdl/__init__.py | 31 ++++++------ b_asic/codegen/vhdl/architecture.py | 49 +++++++++---------- b_asic/codegen/vhdl/common.py | 75 ++++++++++++++--------------- b_asic/codegen/vhdl/entity.py | 7 ++- b_asic/quantization.py | 12 ++++- b_asic/resources.py | 10 ++-- b_asic/schedule.py | 27 +++++++---- examples/fivepointwinograddft.py | 2 +- test/test_quantization.py | 9 ++++ 9 files changed, 122 insertions(+), 100 deletions(-) diff --git a/b_asic/codegen/vhdl/__init__.py b/b_asic/codegen/vhdl/__init__.py index fc42a54f..2cce156a 100644 --- a/b_asic/codegen/vhdl/__init__.py +++ b/b_asic/codegen/vhdl/__init__.py @@ -2,15 +2,14 @@ Module for basic VHDL code generation. """ -from io import TextIOWrapper -from typing import List, Optional, Tuple, Union +from typing import List, Optional, TextIO, Tuple, Union # VHDL code generation tab length VHDL_TAB = r" " def write( - f: TextIOWrapper, + f: TextIO, indent_level: int, text: str, *, @@ -20,13 +19,13 @@ def write( """ Base VHDL code generation utility. - `f'{VHDL_TAB*indent_level}'` is first written to the :class:`io.TextIOWrapper` - object `f`. Immediately after the indentation, `text` is written to `f`. Finally, - `text` is also written to `f`. + ``f'{VHDL_TAB*indent_level}'`` is first written to the TextIO + object *f*. Immediately after the indentation, *text* is written to *f*. Finally, + *text* is also written to *f*. Parameters ---------- - f : :class:`io.TextIOWrapper` + f : TextIO The file object to emit VHDL code to. indent_level : int Indentation level to use. Exactly ``f'{VHDL_TAB*indent_level}`` is written @@ -43,24 +42,22 @@ def write( f.write(f'{VHDL_TAB*indent_level}{text}{end}') -def write_lines( - f: TextIOWrapper, lines: List[Union[Tuple[int, str], Tuple[int, str, str]]] -): +def write_lines(f: TextIO, lines: List[Union[Tuple[int, str], Tuple[int, str, str]]]): """ Multiline VHDL code generation utility. - Each tuple (int, str, [int]) in the list `lines` is written to the - :class:`io.TextIOWrapper` object `f` using the :function:`vhdl.write` function. + Each tuple ``(int, str, [int])`` in the list *lines* is written to the + TextIO object *f* using the :function:`vhdl.write` function. Parameters ---------- - f : :class:`io.TextIOWrapper` + f : TextIO The file object to emit VHDL code to. lines : list of tuple (int,str) [1], or list of tuple (int,str,str) [2] - [1]: The first `int` of the tuple is used as indentation level for the line and - the second `str` of the tuple is the content of the line. - [2]: Same as [1], but the third `str` of the tuple is passed to parameter `end` - when calling :function:`vhdl.write`. + [1]: The first ``int`` of the tuple is used as indentation level for the line + and the second ``str`` of the tuple is the content of the line. + [2]: Same as [1], but the third ``str`` of the tuple is passed to parameter + *end* when calling :function:`vhdl.write`. """ for tpl in lines: if len(tpl) == 2: diff --git a/b_asic/codegen/vhdl/architecture.py b/b_asic/codegen/vhdl/architecture.py index 463f21b3..bc8d8227 100644 --- a/b_asic/codegen/vhdl/architecture.py +++ b/b_asic/codegen/vhdl/architecture.py @@ -1,8 +1,7 @@ """ Module for code generation of VHDL architectures. """ -from io import TextIOWrapper -from typing import TYPE_CHECKING, Dict, Iterable, Set, Tuple, cast +from typing import TYPE_CHECKING, Dict, List, Set, TextIO, Tuple, cast from b_asic.codegen.vhdl import common, write, write_lines from b_asic.process import MemoryVariable, PlainMemoryVariable @@ -12,8 +11,8 @@ if TYPE_CHECKING: def memory_based_storage( - f: TextIOWrapper, - assignment: Iterable["ProcessCollection"], + f: TextIO, + assignment: List["ProcessCollection"], entity_name: str, word_length: int, read_ports: int, @@ -26,9 +25,9 @@ def memory_based_storage( Parameters ---------- - f : TextIOWrapper - File object (or other TextIOWrapper object) to write the architecture onto. - assignment : dict + f : TextIO + File object (or other TextIO object) to write the architecture onto. + assignment : list A possible cell assignment to use when generating the memory based storage. The cell assignment is a dictionary int to ProcessCollection where the integer corresponds to the cell to assign all MemoryVariables in corresponding process @@ -58,7 +57,7 @@ def memory_based_storage( write(f, 0, f'architecture {architecture_name} of {entity_name} is', end='\n\n') # - # Architecture declerative region begin + # Architecture declarative region begin # write(f, 1, '-- HDL memory description') common.constant_declaration( @@ -70,7 +69,7 @@ def memory_based_storage( common.type_declaration( f, 'mem_type', 'array(0 to MEM_DEPTH-1) of std_logic_vector(MEM_WL-1 downto 0)' ) - common.signal_decl( + common.signal_declaration( f, name='memory', signal_type='mem_type', @@ -78,25 +77,25 @@ def memory_based_storage( vivado_ram_style='distributed', ) for i in range(read_ports): - common.signal_decl( + common.signal_declaration( f, f'read_port_{i}', 'std_logic_vector(MEM_WL-1 downto 0)', name_pad=14 ) - common.signal_decl( + common.signal_declaration( f, f'read_adr_{i}', f'integer range 0 to {schedule_time}-1', name_pad=14 ) - common.signal_decl(f, f'read_en_{i}', 'std_logic', name_pad=14) + common.signal_declaration(f, f'read_en_{i}', 'std_logic', name_pad=14) for i in range(write_ports): - common.signal_decl( + common.signal_declaration( f, f'write_port_{i}', 'std_logic_vector(MEM_WL-1 downto 0)', name_pad=14 ) - common.signal_decl( + common.signal_declaration( f, f'write_adr_{i}', f'integer range 0 to {schedule_time}-1', name_pad=14 ) - common.signal_decl(f, f'write_en_{i}', 'std_logic', name_pad=14) + common.signal_declaration(f, f'write_en_{i}', 'std_logic', name_pad=14) # Schedule time counter write(f, 1, '-- Schedule counter', start='\n') - common.signal_decl( + common.signal_declaration( f, name='schedule_cnt', signal_type=f'integer range 0 to {schedule_time}-1', @@ -107,7 +106,7 @@ def memory_based_storage( if input_sync: write(f, 1, '-- Input synchronization', start='\n') for i in range(read_ports): - common.signal_decl( + common.signal_declaration( f, f'p_{i}_in_sync', 'std_logic_vector(WL-1 downto 0)', name_pad=14 ) @@ -191,7 +190,7 @@ def memory_based_storage( f, [ (4, f'-- {mv!r}'), - (4, f'when {(mv.start_time) % schedule_time} =>'), + (4, f'when {mv.start_time % schedule_time} =>'), (5, f'write_adr_0 <= {i};'), (5, 'write_en_0 <= \'1\';'), ], @@ -283,7 +282,7 @@ def memory_based_storage( def register_based_storage( - f: TextIOWrapper, + f: TextIO, forward_backward_table: "_ForwardBackwardTable", entity_name: str, word_length: int, @@ -326,7 +325,7 @@ def register_based_storage( # Schedule time counter write(f, 1, '-- Schedule counter') - common.signal_decl( + common.signal_declaration( f, name='schedule_cnt', signal_type=f'integer range 0 to {schedule_time}-1', @@ -341,7 +340,7 @@ def register_based_storage( name='shift_reg_type', alias=f'array(0 to {reg_cnt}-1) of std_logic_vector(WL-1 downto 0)', ) - common.signal_decl( + common.signal_declaration( f, name='shift_reg', signal_type='shift_reg_type', @@ -350,7 +349,7 @@ def register_based_storage( # Back edge mux decoder write(f, 1, '-- Back-edge mux select signal', start='\n') - common.signal_decl( + common.signal_declaration( f, name='back_edge_mux_sel', signal_type=f'integer range 0 to {len(back_edges)}', @@ -359,7 +358,7 @@ def register_based_storage( # Output mux selector write(f, 1, '-- Output mux select signal', start='\n') - common.signal_decl( + common.signal_declaration( f, name='out_mux_sel', signal_type=f'integer range 0 to {len(output_regs) - 1}', @@ -478,7 +477,7 @@ def register_based_storage( ) # Output multiplexer decoding logic - write(f, 1, '-- Output muliplexer decoding logic', start='\n') + write(f, 1, '-- Output multiplexer decoding logic', start='\n') common.synchronous_process_prologue(f, clk='clk', name='out_mux_decode_proc') write(f, 3, 'case schedule_cnt is') for i, entry in enumerate(forward_backward_table): @@ -490,7 +489,7 @@ def register_based_storage( common.synchronous_process_epilogue(f, clk='clk', name='out_mux_decode_proc') # Output multiplexer logic - write(f, 1, '-- Output muliplexer', start='\n') + write(f, 1, '-- Output multiplexer', start='\n') common.synchronous_process_prologue( f, clk='clk', diff --git a/b_asic/codegen/vhdl/common.py b/b_asic/codegen/vhdl/common.py index 30b5980b..a1ac777c 100644 --- a/b_asic/codegen/vhdl/common.py +++ b/b_asic/codegen/vhdl/common.py @@ -4,20 +4,19 @@ Generation of common VHDL constructs import re from datetime import datetime -from io import TextIOWrapper from subprocess import PIPE, Popen -from typing import Any, Optional, Set, Tuple +from typing import Any, Optional, Set, TextIO, Tuple from b_asic.codegen.vhdl import write, write_lines -def b_asic_preamble(f: TextIOWrapper): +def b_asic_preamble(f: TextIO): """ Write a standard BASIC VHDL preamble comment. Parameters ---------- - f : :class:`io.TextIOWrapper` + f : TextIO The file object to write the header to. """ # Try to acquire the current git commit hash @@ -47,7 +46,7 @@ def b_asic_preamble(f: TextIOWrapper): def ieee_header( - f: TextIOWrapper, + f: TextIO, std_logic_1164: bool = True, numeric_std: bool = True, ): @@ -57,8 +56,8 @@ def ieee_header( Parameters ---------- - f : :class:`io.TextIOWrapper` - The TextIOWrapper object to write the IEEE header to. + f : TextIO + The TextIO object to write the IEEE header to. std_logic_1164 : bool, default: True Include the std_logic_1164 header. numeric_std : bool, default: True @@ -72,8 +71,8 @@ def ieee_header( write(f, 0, '') -def signal_decl( - f: TextIOWrapper, +def signal_declaration( + f: TextIO, name: str, signal_type: str, default_value: Optional[str] = None, @@ -88,8 +87,8 @@ def signal_decl( Parameters ---------- - f : :class:`io.TextIOWrapper` - The TextIOWrapper object to write the IEEE header to. + f : TextIO + The TextIO object to write the IEEE header to. name : str Signal name. signal_type : str @@ -132,7 +131,7 @@ def signal_decl( def constant_declaration( - f: TextIOWrapper, + f: TextIO, name: str, signal_type: str, value: Any, @@ -144,8 +143,8 @@ def constant_declaration( Parameters ---------- - f : :class:`io.TextIOWrapper` - The TextIOWrapper object to write the constant declaration to. + f : TextIO + The TextIO object to write the constant declaration to. name : str Signal name. signal_type : str @@ -160,7 +159,7 @@ def constant_declaration( def type_declaration( - f: TextIOWrapper, + f: TextIO, name: str, alias: str, ): @@ -169,8 +168,8 @@ def type_declaration( Parameters ---------- - f : :class:`io.TextIOWrapper` - The TextIOWrapper object to write the type declaration to. + f : TextIO + The TextIO object to write the type declaration to. name : str Type name alias. alias : str @@ -180,7 +179,7 @@ def type_declaration( def process_prologue( - f: TextIOWrapper, + f: TextIO, sensitivity_list: str, indent: int = 1, name: Optional[str] = None, @@ -192,8 +191,8 @@ def process_prologue( Parameters ---------- - f : :class:`io.TextIOWrapper` - The TextIOWrapper object to write the type declaration to. + f : TextIO + The TextIO object to write the type declaration to. sensitivity_list : str Content of the process sensitivity list. indent : int, default: 1 @@ -209,7 +208,7 @@ def process_prologue( def process_epilogue( - f: TextIOWrapper, + f: TextIO, sensitivity_list: Optional[str] = None, indent: int = 1, name: Optional[str] = None, @@ -217,8 +216,8 @@ def process_epilogue( """ Parameters ---------- - f : :class:`io.TextIOWrapper` - The TextIOWrapper object to write the type declaration to. + f : TextIO + The TextIO object to write the type declaration to. sensitivity_list : str Content of the process sensitivity list. Not needed when writing the epilogue. indent : int, default: 1 @@ -236,7 +235,7 @@ def process_epilogue( def synchronous_process_prologue( - f: TextIOWrapper, + f: TextIO, clk: str, indent: int = 1, name: Optional[str] = None, @@ -251,8 +250,8 @@ def synchronous_process_prologue( Parameters ---------- - f : :class:`io.TextIOWrapper` - The TextIOWrapper to write the VHDL code onto. + f : TextIO + The TextIO to write the VHDL code onto. clk : str Name of the clock. indent : int, default: 1 @@ -265,7 +264,7 @@ def synchronous_process_prologue( def synchronous_process_epilogue( - f: TextIOWrapper, + f: TextIO, clk: Optional[str], indent: int = 1, name: Optional[str] = None, @@ -278,8 +277,8 @@ def synchronous_process_epilogue( Parameters ---------- - f : :class:`io.TextIOWrapper` - The TextIOWrapper to write the VHDL code onto. + f : TextIO + The TextIO to write the VHDL code onto. clk : str Name of the clock. indent : int, default: 1 @@ -293,7 +292,7 @@ def synchronous_process_epilogue( def synchronous_process( - f: TextIOWrapper, + f: TextIO, clk: str, body: str, indent: int = 1, @@ -307,8 +306,8 @@ def synchronous_process( Parameters ---------- - f : :class:`io.TextIOWrapper` - The TextIOWrapper to write the VHDL code onto. + f : TextIO + The TextIO to write the VHDL code onto. clk : str Name of the clock. body : str @@ -326,7 +325,7 @@ def synchronous_process( def synchronous_memory( - f: TextIOWrapper, + f: TextIO, clk: str, read_ports: Set[Tuple[str, str, str]], write_ports: Set[Tuple[str, str, str]], @@ -337,8 +336,8 @@ def synchronous_memory( Parameters ---------- - f : :class:`io.TextIOWrapper` - The TextIOWrapper to write the VHDL code onto. + f : TextIO + The TextIO to write the VHDL code onto. clk : str Name of clock identifier to the synchronous memory. read_ports : Set[Tuple[str,str]] @@ -373,7 +372,7 @@ def synchronous_memory( def asynchronous_read_memory( - f: TextIOWrapper, + f: TextIO, clk: str, read_ports: Set[Tuple[str, str, str]], write_ports: Set[Tuple[str, str, str]], @@ -384,8 +383,8 @@ def asynchronous_read_memory( Parameters ---------- - f : :class:`io.TextIOWrapper` - The TextIOWrapper to write the VHDL code onto. + f : TextIO + The TextIO to write the VHDL code onto. clk : str Name of clock identifier to the synchronous memory. read_ports : Set[Tuple[str,str]] diff --git a/b_asic/codegen/vhdl/entity.py b/b_asic/codegen/vhdl/entity.py index 8e2328e8..f13d1a17 100644 --- a/b_asic/codegen/vhdl/entity.py +++ b/b_asic/codegen/vhdl/entity.py @@ -1,8 +1,7 @@ """ Module for code generation of VHDL entity declarations """ -from io import TextIOWrapper -from typing import Set +from typing import Set, TextIO from b_asic.codegen.vhdl import VHDL_TAB, write_lines from b_asic.port import Port @@ -11,7 +10,7 @@ from b_asic.resources import ProcessCollection def memory_based_storage( - f: TextIOWrapper, entity_name: str, collection: ProcessCollection, word_length: int + f: TextIO, entity_name: str, collection: ProcessCollection, word_length: int ): # Check that this is a ProcessCollection of (Plain)MemoryVariables is_memory_variable = all( @@ -79,6 +78,6 @@ def memory_based_storage( def register_based_storage( - f: TextIOWrapper, entity_name: str, collection: ProcessCollection, word_length: int + f: TextIO, entity_name: str, collection: ProcessCollection, word_length: int ): memory_based_storage(f, entity_name, collection, word_length) diff --git a/b_asic/quantization.py b/b_asic/quantization.py index 2e70ad4c..5de9a517 100644 --- a/b_asic/quantization.py +++ b/b_asic/quantization.py @@ -19,11 +19,14 @@ class Quantization(Enum): "Magnitude truncation, i.e., round towards zero." JAMMING = 4 - "Jamming/von Neumann rounding, i.e., set the LSB to one" + "Jamming/von Neumann rounding, i.e., set the LSB to one." UNBIASED_ROUNDING = 5 "Unbiased rounding, i.e., tie rounds towards even." + UNBIASED_JAMMING = 6 + "Unbiased jamming/von Neumann rounding." + class Overflow(Enum): """Overflow types.""" @@ -125,8 +128,13 @@ def quantize( v = math.ceil(v) elif quantization is Quantization.JAMMING: v = math.floor(v) | 1 - else: # Quantization.UNBIASED_ROUNDING + elif quantization is Quantization.UNBIASED_ROUNDING: v = round(v) + elif quantization is Quantization.UNBIASED_JAMMING: + f = math.floor(v) + v = f if v - f == 0 else f | 1 + else: + raise TypeError("Unknown quantization method: {quantization!r}") v = v / b i = 2 ** (integer_bits - 1) diff --git a/b_asic/resources.py b/b_asic/resources.py index 51d05523..ef1204ab 100644 --- a/b_asic/resources.py +++ b/b_asic/resources.py @@ -651,8 +651,12 @@ class ProcessCollection: ) _ax.grid(True) # type: ignore - _ax.xaxis.set_major_locator(MaxNLocator(integer=True)) # type: ignore - _ax.yaxis.set_major_locator(MaxNLocator(integer=True)) # type: ignore + _ax.xaxis.set_major_locator( + MaxNLocator(integer=True, min_n_ticks=1) + ) # type: ignore + _ax.yaxis.set_major_locator( + MaxNLocator(integer=True, min_n_ticks=1) + ) # type: ignore _ax.set_xlim(0, self._schedule_time) # type: ignore if row is None: _ax.set_ylim(0.25, len(self._collection) + 0.75) # type: ignore @@ -1154,7 +1158,7 @@ class ProcessCollection: Name used for the VHDL entity. word_length : int Word length of the memory variable objects. - assignment : set + assignment : list A possible cell assignment to use when generating the memory based storage. The cell assignment is a dictionary int to ProcessCollection where the integer corresponds to the cell to assign all MemoryVariables in diff --git a/b_asic/schedule.py b/b_asic/schedule.py index 253c1074..db668d10 100644 --- a/b_asic/schedule.py +++ b/b_asic/schedule.py @@ -7,7 +7,7 @@ Contains the schedule class for scheduling operations in an SFG. import io import sys from collections import defaultdict -from typing import Dict, List, Optional, Sequence, Tuple, cast +from typing import Dict, List, Optional, Sequence, Tuple, Union, cast import matplotlib.pyplot as plt import numpy as np @@ -38,9 +38,15 @@ from b_asic.special_operations import Delay, Input, Output from b_asic.types import TypeName # Need RGB from 0 to 1 -_EXECUTION_TIME_COLOR = tuple(c / 255 for c in EXECUTION_TIME_COLOR) -_LATENCY_COLOR = tuple(c / 255 for c in LATENCY_COLOR) -_SIGNAL_COLOR = tuple(c / 255 for c in SIGNAL_COLOR) +_EXECUTION_TIME_COLOR: Union[ + Tuple[float, float, float], Tuple[float, float, float, float] +] = tuple(float(c / 255) for c in EXECUTION_TIME_COLOR) +_LATENCY_COLOR: Union[ + Tuple[float, float, float], Tuple[float, float, float, float] +] = tuple(float(c / 255) for c in LATENCY_COLOR) +_SIGNAL_COLOR: Union[ + Tuple[float, float, float], Tuple[float, float, float, float] +] = tuple(float(c / 255) for c in SIGNAL_COLOR) def _laps_default(): @@ -443,11 +449,11 @@ class Schedule: def get_possible_time_resolution_decrements(self) -> List[int]: """Return a list with possible factors to reduce time resolution.""" vals = self._get_all_times() - maxloop = min(val for val in vals if val) - if maxloop <= 1: + max_loop = min(val for val in vals if val) + if max_loop <= 1: return [1] ret = [1] - for candidate in range(2, maxloop + 1): + for candidate in range(2, max_loop + 1): if not any(val % candidate for val in vals): ret.append(candidate) return ret @@ -700,7 +706,6 @@ class Schedule: ) source_port = inport.signals[0].source - source_end_time = None if source_port.operation.graph_id in non_schedulable_ops: source_end_time = 0 else: @@ -793,7 +798,9 @@ class Schedule: """ return ProcessCollection( { - OperatorProcess(start_time, self._sfg.find_by_id(graph_id)) + OperatorProcess( + start_time, cast(Operation, self._sfg.find_by_id(graph_id)) + ) for graph_id, start_time in self._start_times.items() }, self.schedule_time, @@ -987,7 +994,7 @@ class Schedule: + (OPERATION_GAP if operation_gap is None else operation_gap) ) ax.axis([-1, self._schedule_time + 1, y_position_max, 0]) # Inverted y-axis - ax.xaxis.set_major_locator(MaxNLocator(integer=True)) + ax.xaxis.set_major_locator(MaxNLocator(integer=True, min_n_ticks=1)) ax.axvline( 0, linestyle="--", diff --git a/examples/fivepointwinograddft.py b/examples/fivepointwinograddft.py index 8093a2e1..2d26c4de 100644 --- a/examples/fivepointwinograddft.py +++ b/examples/fivepointwinograddft.py @@ -190,6 +190,7 @@ mem_vars = schedule.get_memory_variables() mem_vars.show(title="All memory variables") direct, mem_vars = mem_vars.split_on_length() mem_vars.show(title="Non-zero time memory variables") +direct.show(title="Direct interconnects") mem_vars_set = mem_vars.split_on_ports(read_ports=1, write_ports=1, total_ports=2) memories = [] @@ -200,7 +201,6 @@ for i, mem in enumerate(mem_vars_set): memory.assign("left_edge") memory.show_content(title=f"Assigned {memory.entity_name}") -direct.show(title="Direct interconnects") arch = Architecture( {addsub, butterfly, multiplier, pe_in, pe_out}, diff --git a/test/test_quantization.py b/test/test_quantization.py index 2923787b..b668a45d 100644 --- a/test/test_quantization.py +++ b/test/test_quantization.py @@ -8,10 +8,14 @@ def test_quantization(): assert quantize(a, 4, quantization=Quantization.ROUNDING) == 0.3125 assert quantize(a, 4, quantization=Quantization.MAGNITUDE_TRUNCATION) == 0.25 assert quantize(a, 4, quantization=Quantization.JAMMING) == 0.3125 + assert quantize(a, 4, quantization=Quantization.UNBIASED_ROUNDING) == 0.3125 + assert quantize(a, 4, quantization=Quantization.UNBIASED_JAMMING) == 0.3125 assert quantize(-a, 4, quantization=Quantization.TRUNCATION) == -0.3125 assert quantize(-a, 4, quantization=Quantization.ROUNDING) == -0.3125 assert quantize(-a, 4, quantization=Quantization.MAGNITUDE_TRUNCATION) == -0.25 assert quantize(-a, 4, quantization=Quantization.JAMMING) == -0.3125 + assert quantize(-a, 4, quantization=Quantization.UNBIASED_ROUNDING) == -0.3125 + assert quantize(-a, 4, quantization=Quantization.UNBIASED_JAMMING) == -0.3125 assert quantize(complex(a, -a), 4) == complex(0.25, -0.3125) assert quantize( complex(a, -a), 4, quantization=Quantization.MAGNITUDE_TRUNCATION @@ -26,3 +30,8 @@ def test_quantization(): ) == 0.9375 ) + + assert quantize(0.3125, 3, quantization=Quantization.ROUNDING) == 0.375 + assert quantize(0.3125, 3, quantization=Quantization.UNBIASED_ROUNDING) == 0.25 + assert quantize(0.25, 3, quantization=Quantization.JAMMING) == 0.375 + assert quantize(0.25, 3, quantization=Quantization.UNBIASED_JAMMING) == 0.25 -- GitLab