diff --git a/docs/algorithms/index_information.rst b/docs/algorithms/index_information.rst index e67822fde..8b2cd3c59 100644 --- a/docs/algorithms/index_information.rst +++ b/docs/algorithms/index_information.rst @@ -5,6 +5,7 @@ Network information extraction :maxdepth: 1 simulation + simulation_sequential pattern_generation dont_cares cut_enumeration diff --git a/docs/algorithms/simulation_sequential.rst b/docs/algorithms/simulation_sequential.rst new file mode 100644 index 000000000..2855a187c --- /dev/null +++ b/docs/algorithms/simulation_sequential.rst @@ -0,0 +1,72 @@ +Sequential simulation +--------------------- + +**Header:** ``mockturtle/algorithms/simulation_sequential.hpp`` + +``simulate`` evaluates the combinational logic of a network exactly once and has no notion of a register. On a sequential network it never assigns the register outputs at all, so every value in their fanout cone is meaningless. + +``simulate_sequential`` runs the network over a number of clock cycles instead. Every register starts at its reset value, the combinational logic is evaluated once per cycle, the primary outputs are recorded, and the register inputs are latched into the register outputs for the next cycle. + +It returns a ``simulate_sequential_result``, which carries two traces indexed by clock cycle: ``outputs[cycle][index]`` is the value primary output ``index`` took in that cycle, and ``states[cycle][index]`` the value register ``index`` held while that cycle was evaluated. The state trace is one entry longer than the output trace, because simulating *n* cycles crosses *n + 1* state boundaries -- ``reset_state()`` is the one the run started from and ``final_state()`` the one it ended in. + +**Examples** + +A design with no primary inputs runs off its reset state alone. A 4-bit LFSR seeded with ``0b0001`` walks through all 15 of its non-zero states: + +.. code-block:: c++ + + sequential lfsr = ...; + + auto const result = simulate_sequential( lfsr, 15, default_simulator( std::vector{} ) ); + + for ( auto const& outputs : result.outputs ) + { + std::cout << outputs[0]; + } + +The state trace answers what the registers were doing while that happened, and where they ended up: + +.. code-block:: c++ + + for ( auto const& state : result.states ) + { + std::cout << fmt::format( "{}\n", fmt::join( state, "" ) ); + } + + assert( result.final_state() == result.reset_state() ); // a full period + +Primary inputs that change from one cycle to the next are supplied by ``stimulus_simulator``, which holds one assignment vector per cycle and repeats its last one for the rest of the run: + +.. code-block:: c++ + + sequential shift_register = ...; + + /* a single 1 on the input, then silence */ + stimulus_simulator sim( { { true }, { false } } ); + + auto const result = simulate_sequential( shift_register, 6, sim ); + +Any simulator that works with ``simulate`` works here too, holding its assignment for the whole run. Truth tables, for instance, give the outputs of each cycle as a function of the primary inputs: + +.. code-block:: c++ + + auto const result = simulate_sequential( + ntk, 3, default_simulator( ntk.num_pis() ) ); + +**Result** + +.. doxygenstruct:: mockturtle::simulate_sequential_result + :members: + +**Parameters** + +.. doxygenstruct:: mockturtle::simulate_sequential_params + :members: + +**Simulators** + +.. doxygenclass:: mockturtle::stimulus_simulator + +**Algorithm** + +.. doxygenfunction:: mockturtle::simulate_sequential diff --git a/docs/changelog.rst b/docs/changelog.rst index 0ea2e3d8c..5c939a30d 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -40,6 +40,7 @@ v0.4 (not yet released) - Adding don't care support in rewriting (`map`, `rewrite`) `#623 `_ - XAG balancing (`xag_balance`) `#627 `_ - XAG resubstitution (`xag_resubstitution`) `#658 `_ + - Cycle-accurate simulation of sequential networks (`simulate_sequential`) `#708 `_ * I/O: - Write gates to GENLIB file (`write_genlib`) `#606 `_ * Views: diff --git a/include/mockturtle/algorithms/simulation_sequential.hpp b/include/mockturtle/algorithms/simulation_sequential.hpp new file mode 100644 index 000000000..4ada5aa5e --- /dev/null +++ b/include/mockturtle/algorithms/simulation_sequential.hpp @@ -0,0 +1,314 @@ +/* mockturtle: C++ logic network library + * Copyright (C) 2018-2022 EPFL + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + */ + +/*! + \file simulation_sequential.hpp + \brief Cycle-accurate simulation of sequential networks + + \author Marcel Walter +*/ + +#pragma once + +#include "../networks/sequential.hpp" +#include "../traits.hpp" +#include "../utils/node_map.hpp" +#include "simulation.hpp" + +#include +#include +#include +#include +#include +#include +#include + +namespace mockturtle +{ + +/*! \brief Simulates Boolean assignments that change from cycle to cycle. + * + * A simulator for `simulate_sequential` holding one assignment vector per clock + * cycle. Cycles past the end of the stimulus repeat its last assignment, so a + * stimulus shorter than the run holds its final value for the remainder of it. + */ +class stimulus_simulator +{ +public: + stimulus_simulator() = delete; + + explicit stimulus_simulator( std::vector> stimulus ) + : _stimulus( std::move( stimulus ) ) + { + assert( !_stimulus.empty() && "a stimulus needs at least one assignment" ); + } + + bool compute_constant( bool value ) const { return value; } + + bool compute_pi( uint32_t index, uint32_t cycle ) const + { + return _stimulus[std::min( cycle, _stimulus.size() - 1 )][index]; + } + + bool compute_not( bool value ) const { return !value; } + +private: + std::vector> _stimulus; +}; + +/*! \brief The result of simulating a sequential network. + * + * Both traces are indexed by clock cycle first. `outputs[cycle][index]` is the + * value primary output `index` took in that cycle, and `states[cycle][index]` the + * value register `index` held while that cycle was evaluated. + * + * `states` is one entry longer than `outputs`: simulating `n` cycles crosses + * `n + 1` state boundaries. `states.front()` is the reset state the run started + * from and `states.back()` the state it ended in, so a run of zero cycles still + * reports the reset state and nothing else. + */ +template +struct simulate_sequential_result +{ + /*! \brief Primary output values, one vector per clock cycle. */ + std::vector> outputs; + + /*! \brief Register values, one vector per state boundary. */ + std::vector> states; + + /*! \brief Number of clock cycles simulated. */ + uint32_t num_cycles() const + { + return static_cast( outputs.size() ); + } + + /*! \brief The state the registers were reset to. */ + std::vector const& reset_state() const + { + assert( !states.empty() && "the state trace always holds the reset state" ); + return states.front(); + } + + /*! \brief The state the registers held after the last cycle. */ + std::vector const& final_state() const + { + assert( !states.empty() && "the state trace always holds the reset state" ); + return states.back(); + } +}; + +/*! \brief Parameters for `simulate_sequential`. */ +struct simulate_sequential_params +{ + /*! \brief Value a register starts at when its reset value is not defined. + * + * A register may declare no reset value at all -- `register_init::dont_care` + * or `register_init::unknown`, which is what `register_t` defaults to and what + * an AIGER latch with a nondeterministic reset reads back as. Simulation + * needs a concrete value, so this is the one it uses. + */ + bool undefined_reset_value{ false }; +}; + +namespace detail +{ + +/*! \brief Whether a simulator can produce a different value in every cycle. */ +template +struct has_compute_pi_at_cycle : std::false_type +{ +}; + +template +struct has_compute_pi_at_cycle().compute_pi( uint32_t(), uint32_t() ) )>> : std::true_type +{ +}; + +template +inline constexpr bool has_compute_pi_at_cycle_v = has_compute_pi_at_cycle::value; + +/*! \brief Evaluates every gate of the network from the values of its inputs. */ +template +void simulate_gates( Ntk const& ntk, node_map& node_to_value ) +{ + ntk.foreach_gate( [&]( auto const& n ) { + std::vector fanin_values( ntk.fanin_size( n ) ); + ntk.foreach_fanin( n, [&]( auto const& f, auto i ) { + fanin_values[i] = node_to_value[f]; + } ); + node_to_value[n] = ntk.compute( n, fanin_values.begin(), fanin_values.end() ); + } ); +} + +} /* namespace detail */ + +/*! \brief Simulates a sequential network over a number of clock cycles. + * + * Every register starts at its reset value, the combinational logic is evaluated + * once per cycle, the primary outputs are recorded, and the register inputs are + * latched into the register outputs for the next cycle. + * + * This is what distinguishes it from `simulate`, which evaluates the + * combinational logic exactly once and has no notion of a register: on a + * sequential network `simulate` never assigns the register outputs at all, and + * every value in their fanout cone is meaningless. + * + * The simulator follows the same concept as for `simulate`, with one addition. + * If it provides `compute_pi( index, cycle )`, that overload is used and the + * primary inputs may take a different value in every cycle -- see + * `stimulus_simulator`. A simulator offering only `compute_pi( index )` holds + * its assignment for the whole run, which is what a design with no primary + * inputs, such as an LFSR, wants anyway. + * + * A register whose reset value is undefined starts at + * `simulate_sequential_params::undefined_reset_value`. + * + * **Required network functions:** + * - `num_registers` + * - `register_at` + * - `foreach_ro` + * - `foreach_ri` + * - `foreach_pi` + * - `foreach_po` + * - `foreach_gate` + * - `foreach_fanin` + * - `fanin_size` + * - `get_constant` + * - `constant_value` + * - `get_node` + * - `is_complemented` + * - `compute` + * + \verbatim embed:rst + + Example + + .. code-block:: c++ + + sequential aig = ...; // a 4-bit LFSR, say + + auto const result = simulate_sequential( aig, 15, default_simulator( std::vector{} ) ); + + for ( auto const& outputs : result.outputs ) + { + std::cout << outputs[0]; + } + + // where it ended up + auto const& state = result.final_state(); + \endverbatim + * + * \param ntk The sequential network to simulate + * \param num_cycles Number of clock cycles to run + * \param sim The simulator + * \param ps Parameters + * \return The primary output values and the register values, per clock cycle + */ +template> +simulate_sequential_result simulate_sequential( Ntk const& ntk, uint32_t num_cycles, Simulator const& sim = Simulator(), simulate_sequential_params const& ps = {} ) +{ + static_assert( is_network_type_v, "Ntk is not a network type" ); + static_assert( has_num_registers_v, "Ntk does not implement the num_registers method" ); + static_assert( has_foreach_ro_v, "Ntk does not implement the foreach_ro method" ); + static_assert( has_foreach_ri_v, "Ntk does not implement the foreach_ri method" ); + static_assert( has_foreach_pi_v, "Ntk does not implement the foreach_pi method" ); + static_assert( has_foreach_po_v, "Ntk does not implement the foreach_po method" ); + static_assert( has_foreach_gate_v, "Ntk does not implement the foreach_gate method" ); + static_assert( has_foreach_fanin_v, "Ntk does not implement the foreach_fanin method" ); + static_assert( has_fanin_size_v, "Ntk does not implement the fanin_size method" ); + static_assert( has_get_constant_v, "Ntk does not implement the get_constant method" ); + static_assert( has_constant_value_v, "Ntk does not implement the constant_value method" ); + static_assert( has_get_node_v, "Ntk does not implement the get_node method" ); + static_assert( has_is_complemented_v, "Ntk does not implement the is_complemented method" ); + static_assert( has_compute_v, "Ntk does not implement the compute method for SimulationType" ); + + /* the register state, one entry per register, seeded from the reset values */ + std::vector state( ntk.num_registers() ); + for ( auto i = 0u; i < ntk.num_registers(); ++i ) + { + auto const init = ntk.register_at( i ).init; + bool const value = register_init::is_defined( init ) ? init == register_init::one + : ps.undefined_reset_value; + state[i] = sim.compute_constant( value ); + } + + simulate_sequential_result result; + result.outputs.reserve( num_cycles ); + result.states.reserve( num_cycles + 1u ); + result.states.push_back( state ); + + node_map node_to_value( ntk ); + + auto const evaluate = [&]( auto const& f ) { + return ntk.is_complemented( f ) ? sim.compute_not( node_to_value[f] ) : node_to_value[f]; + }; + + for ( auto cycle = 0u; cycle < num_cycles; ++cycle ) + { + /* constants */ + node_to_value[ntk.get_node( ntk.get_constant( false ) )] = sim.compute_constant( ntk.constant_value( ntk.get_node( ntk.get_constant( false ) ) ) ); + if ( ntk.get_node( ntk.get_constant( false ) ) != ntk.get_node( ntk.get_constant( true ) ) ) + { + node_to_value[ntk.get_node( ntk.get_constant( true ) )] = sim.compute_constant( ntk.constant_value( ntk.get_node( ntk.get_constant( true ) ) ) ); + } + + /* primary inputs */ + ntk.foreach_pi( [&]( auto const& n, auto i ) { + if constexpr ( detail::has_compute_pi_at_cycle_v ) + { + node_to_value[n] = sim.compute_pi( i, cycle ); + } + else + { + node_to_value[n] = sim.compute_pi( i ); + } + } ); + + /* the register outputs hold the state this cycle starts in */ + ntk.foreach_ro( [&]( auto const& n, auto i ) { + node_to_value[n] = state[i]; + } ); + + detail::simulate_gates( ntk, node_to_value ); + + std::vector outputs( ntk.num_pos() ); + ntk.foreach_po( [&]( auto const& f, auto i ) { + outputs[i] = evaluate( f ); + } ); + result.outputs.push_back( std::move( outputs ) ); + + /* latch the register inputs for the next cycle */ + std::vector next( ntk.num_registers() ); + ntk.foreach_ri( [&]( auto const& f, auto i ) { + next[i] = evaluate( f ); + } ); + state = std::move( next ); + result.states.push_back( state ); + } + + return result; +} + +} /* namespace mockturtle */ diff --git a/test/algorithms/simulation_sequential.cpp b/test/algorithms/simulation_sequential.cpp new file mode 100644 index 000000000..b827b5973 --- /dev/null +++ b/test/algorithms/simulation_sequential.cpp @@ -0,0 +1,283 @@ +#include + +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include +#include + +using namespace mockturtle; + +namespace +{ + +/*! \brief Builds a Fibonacci LFSR with taps on the top two bits. + * + * It has no primary inputs at all, so it runs off its reset state alone -- which + * makes it a direct test of whether the reset values are honoured. + */ +sequential lfsr( uint32_t width, uint32_t seed ) +{ + sequential aig; + + std::vector state( width ); + for ( auto i = 0u; i < width; ++i ) + { + state[i] = aig.create_ro(); + } + + auto const feedback = aig.create_xor( state[width - 1], state[width - 2] ); + + /* primary outputs are created before register inputs: both are combinational + outputs of the same network, sliced by position */ + aig.create_po( state[width - 1] ); + + aig.create_ri( feedback ); + for ( auto i = 0u; i + 1 < width; ++i ) + { + aig.create_ri( state[i] ); + } + + for ( auto i = 0u; i < width; ++i ) + { + mockturtle::register_t reg; + reg.init = ( ( seed >> i ) & 1 ) ? register_init::one : register_init::zero; + aig.set_register( i, reg ); + } + + return aig; +} + +/*! \brief Collects the single primary output of every cycle into a bit string. */ +std::string trace_of( simulate_sequential_result const& result ) +{ + std::string bits; + for ( auto const& outputs : result.outputs ) + { + bits += outputs[0] ? '1' : '0'; + } + return bits; +} + +/*! \brief Reads a register state as an integer, register 0 being the low bit. */ +uint32_t state_of( std::vector const& state ) +{ + uint32_t value{ 0 }; + for ( auto i = 0u; i < state.size(); ++i ) + { + value |= static_cast( state[i] ) << i; + } + return value; +} + +} /* namespace */ + +TEST_CASE( "simulate an LFSR from its reset state", "[simulation_sequential]" ) +{ + auto const aig = lfsr( 4, 1 ); + + auto const result = simulate_sequential( aig, 15, default_simulator( std::vector{} ) ); + + CHECK( result.num_cycles() == 15 ); + + /* a maximal-length sequence: 15 states before it comes back around */ + CHECK( trace_of( result ) == "000100110101111" ); + + /* and it does come back around -- cycle 15 repeats cycle 0 */ + auto const two_periods = simulate_sequential( aig, 30, default_simulator( std::vector{} ) ); + CHECK( trace_of( two_periods ).substr( 0, 15 ) == trace_of( two_periods ).substr( 15 ) ); +} + +TEST_CASE( "a different seed shifts the same sequence", "[simulation_sequential]" ) +{ + /* seeding with the second state of the first LFSR must produce the same + sequence one step ahead, which is only true if the reset values are used */ + auto const from_one = simulate_sequential( lfsr( 4, 1 ), 15, default_simulator( std::vector{} ) ); + auto const from_two = simulate_sequential( lfsr( 4, 2 ), 15, default_simulator( std::vector{} ) ); + + CHECK( trace_of( from_one ).substr( 1 ) == trace_of( from_two ).substr( 0, 14 ) ); +} + +TEST_CASE( "a register with no reset value follows the parameter", "[simulation_sequential]" ) +{ + /* a single register that simply holds whatever it was reset to */ + sequential aig; + auto const state = aig.create_ro(); + aig.create_po( state ); + aig.create_ri( state ); + + mockturtle::register_t reg; + reg.init = register_init::unknown; + aig.set_register( 0, reg ); + + simulate_sequential_params ps; + + ps.undefined_reset_value = false; + CHECK( trace_of( simulate_sequential( aig, 3, default_simulator( std::vector{} ), ps ) ) == "000" ); + + ps.undefined_reset_value = true; + CHECK( trace_of( simulate_sequential( aig, 3, default_simulator( std::vector{} ), ps ) ) == "111" ); +} + +TEST_CASE( "simulate a shift register with a per-cycle stimulus", "[simulation_sequential]" ) +{ + /* three registers in a chain: whatever is put in appears at the output three + cycles later */ + sequential aig; + + auto const in = aig.create_pi(); + auto const a = aig.create_ro(); + auto const b = aig.create_ro(); + auto const c = aig.create_ro(); + + aig.create_po( c ); + + aig.create_ri( in ); + aig.create_ri( a ); + aig.create_ri( b ); + + for ( auto i = 0u; i < 3u; ++i ) + { + mockturtle::register_t reg; + reg.init = register_init::zero; + aig.set_register( i, reg ); + } + + /* a single 1 on the input, then silence */ + stimulus_simulator sim( { { true }, { false } } ); + + CHECK( trace_of( simulate_sequential( aig, 6, sim ) ) == "000100" ); +} + +TEST_CASE( "a stimulus shorter than the run holds its last assignment", "[simulation_sequential]" ) +{ + sequential aig; + + auto const in = aig.create_pi(); + auto const state = aig.create_ro(); + + aig.create_po( state ); + aig.create_ri( in ); + + mockturtle::register_t reg; + reg.init = register_init::zero; + aig.set_register( 0, reg ); + + /* One assignment for a four-cycle run: the input stays high after cycle 0. + Spelled through a named vector rather than as `sim( { { true } } )`, which + GCC 12 and older cannot tell apart from a copy construction -- the same + reason `default_simulator` is spelled with an explicit `std::vector{}` + throughout this file. */ + std::vector> const stimulus{ { true } }; + stimulus_simulator sim( stimulus ); + + CHECK( trace_of( simulate_sequential( aig, 4, sim ) ) == "0111" ); +} + +TEST_CASE( "simulate a sequential network with truth tables", "[simulation_sequential]" ) +{ + /* a register holding the AND of the two primary inputs: the output is constant + 0 in the first cycle and the AND from the second one on */ + sequential aig; + + auto const x0 = aig.create_pi(); + auto const x1 = aig.create_pi(); + auto const state = aig.create_ro(); + + aig.create_po( state ); + aig.create_ri( aig.create_and( x0, x1 ) ); + + mockturtle::register_t reg; + reg.init = register_init::zero; + aig.set_register( 0, reg ); + + auto const result = simulate_sequential( + aig, 3, default_simulator( 2 ) ); + + kitty::dynamic_truth_table expected( 2 ); + kitty::create_from_hex_string( expected, "8" ); + + CHECK( kitty::is_const0( result.outputs[0][0] ) ); + CHECK( result.outputs[1][0] == expected ); + CHECK( result.outputs[2][0] == expected ); + + /* the register itself carries the AND from the first cycle on */ + CHECK( kitty::is_const0( result.states[0][0] ) ); + CHECK( result.states[1][0] == expected ); + CHECK( result.final_state()[0] == expected ); +} + +TEST_CASE( "simulating no cycles still reports the reset state", "[simulation_sequential]" ) +{ + auto const result = simulate_sequential( lfsr( 4, 1 ), 0, default_simulator( std::vector{} ) ); + + CHECK( result.num_cycles() == 0 ); + CHECK( result.outputs.empty() ); + + /* zero cycles still cross one state boundary: the one the run started at */ + CHECK( result.states.size() == 1 ); + CHECK( state_of( result.reset_state() ) == 1 ); + CHECK( state_of( result.final_state() ) == 1 ); +} + +TEST_CASE( "the state trace follows the LFSR through its cycle", "[simulation_sequential]" ) +{ + auto const result = simulate_sequential( lfsr( 4, 1 ), 15, default_simulator( std::vector{} ) ); + + /* n cycles cross n + 1 state boundaries */ + CHECK( result.num_cycles() == 15 ); + CHECK( result.states.size() == result.outputs.size() + 1 ); + + /* it starts at its seed and, after a full period, returns to it */ + CHECK( state_of( result.reset_state() ) == 1 ); + CHECK( state_of( result.final_state() ) == 1 ); + + /* every intermediate state is distinct and non-zero -- a maximal-length run */ + std::vector seen; + for ( auto i = 0u; i < result.num_cycles(); ++i ) + { + CHECK( state_of( result.states[i] ) != 0 ); + seen.push_back( state_of( result.states[i] ) ); + } + std::sort( seen.begin(), seen.end() ); + CHECK( std::unique( seen.begin(), seen.end() ) == seen.end() ); +} + +TEST_CASE( "the state trace records what a register held during its cycle", "[simulation_sequential]" ) +{ + /* one register, driven straight from the primary input, and read out on the + primary output: the output of a cycle is the state it started in */ + sequential aig; + + auto const in = aig.create_pi(); + auto const state = aig.create_ro(); + + aig.create_po( state ); + aig.create_ri( in ); + + mockturtle::register_t reg; + reg.init = register_init::zero; + aig.set_register( 0, reg ); + + std::vector> const stimulus{ { true }, { false }, { true } }; + stimulus_simulator sim( stimulus ); + + auto const result = simulate_sequential( aig, 3, sim ); + + CHECK( trace_of( result ) == "010" ); + for ( auto i = 0u; i < result.num_cycles(); ++i ) + { + CHECK( result.states[i][0] == result.outputs[i][0] ); + } + + /* the last input latched but never read out */ + CHECK( result.final_state()[0] == true ); +}