diff --git a/charlib/characterizer/characterizer.py b/charlib/characterizer/characterizer.py index f8e38be7..8e493aaf 100644 --- a/charlib/characterizer/characterizer.py +++ b/charlib/characterizer/characterizer.py @@ -86,7 +86,7 @@ def characterize(self): # Run all simulation jobs and merge each resulting liberty cell group into the library with tqdm(bar_format='{l_bar}{bar}| {n_fmt}/{total_fmt} [{elapsed}<{remaining}]', total=len(simulation_tasks), desc="Characterizing") as progress_bar: - with ProcessPoolExecutor(max_workers=self.settings.jobs) as executor: + with ProcessPoolExecutor(max_workers=self.settings.jobs, max_tasks_per_child=1) as executor: futures = [executor.submit(task, *args) for (task, *args) in simulation_tasks] for future in as_completed(futures): try: @@ -134,8 +134,9 @@ def __init__(self, **kwargs): self.debug = kwargs.pop('debug', False) self.debug_dir = Path(kwargs.pop('debug_dir', 'debug')) self.quiet = kwargs.pop('quiet', False) - self.cell_defaults = kwargs.get('cell_defaults', {}) + self.dry_run = kwargs.pop('dry_run', False) self.omit_on_failure = kwargs.get('omit_on_failure', False) + self.cell_defaults = kwargs.get('cell_defaults', {}) # Simulation procedures self.simulation = SimulationSettings(**kwargs.get('simulation', {})) diff --git a/charlib/characterizer/procedures/combinational/delay.py b/charlib/characterizer/procedures/combinational/delay.py index 2cd60a0f..ac1df397 100644 --- a/charlib/characterizer/procedures/combinational/delay.py +++ b/charlib/characterizer/procedures/combinational/delay.py @@ -47,7 +47,7 @@ def measure_delays_for_path_with_criterion(cell, config, settings, variation, pa Default max. """ # Set up key parameters - [input_pin, _, output_pin, _] = path + [input_pin, _, output_pin, output_transition] = path data_slew = variation['data_slews'] * settings.units.time load = variation['loads'] * settings.units.capacitance t_sim_end = max(variation['transient_sim_end_time'] * settings.units.time, 1000*data_slew) @@ -131,38 +131,45 @@ def measure_delays_for_path_with_criterion(cell, config, settings, variation, pa raise ValueError(f'Unable to connect unrecognized pin {pin.name} in cell {cell.name}') circuit.X('dut', cell.name, *connections) - # Run the simulation, taking all measurements + # Build the simulation simulator = PySpice.Simulator.factory(simulator=settings.simulation.backend) simulation = simulator.simulation( circuit, temperature=settings.temperature, nominal_temperature=settings.temperature ) - simulation.options('autostop', 'nopage', 'nomod', post=1, ingold=2, trtol=1) + simulation.options('autostop', trtol=1) for measure in measurements: simulation.measure(*measure, run=False) simulation.transient(step_time=data_slew/8, end_time=t_sim_end, run=False) stable_pins_map_str = ', '.join(['='.join([pin, state]) for pin, state in pin_map.stable_inputs.items()]) + + if settings.debug: + debug_path = settings.debug_dir / cell.name / __name__.split('.')[-1] + debug_path.mkdir(parents=True, exist_ok=True) + with open(debug_path / f'slew = {data_slew} load = {load}.sp', 'w', encoding='utf-8') as file: + file.write(str(simulation)) + + # Skip simulation if this is a dry-run + if settings.dry_run: + # TODO: Display a message if not settings.quiet + continue + + # Run the simulation, taking all measurements try: analyses[stable_pins_map_str] = simulator.run(simulation) except Exception as e: msg = f'Procedure measure_worst_case_delay_for_path failed for cell {cell.name} ' \ f'with variation {variation}, pin states {state_map}' - if settings.debug: - debug_path = settings.debug_dir / cell.name / __name__.split('.')[-1] - debug_path.mkdir(parents=True, exist_ok=True) - with open(debug_path / f'slew = {data_slew} load = {load}.sp', 'w', encoding='utf-8') as file: - file.write(str(simulation)) raise ProcedureFailedException(msg) from e # Select the worst-case delays and add to LUTs result = cell.liberty - result.group('pin', output_pin).add_group('timing', f'/* {input_pin} */') # FIXME: This is a \ - # hack to allow multiple timing groups while the liberty API doesn't yet support multiple - # groups with the same name and no id. In practice timing groups are distinguished by - # their related_pin attribute. - result.group('pin', output_pin).group('timing', f'/* {input_pin} */').add_attribute('related_pin', input_pin) # FIXME + timing_group = liberty.Group('timing') + timing_group.add_attribute('related_pin', input_pin) + timing_type = 'combinational_rise' if output_transition == '01' else 'combinational_fall' + timing_group.add_attribute('timing_type', timing_type) for name in measurement_names: # Get the worst delay & plot io if 'io' in config.plots: @@ -178,14 +185,23 @@ def measure_delays_for_path_with_criterion(cell, config, settings, variation, pa plt.close(fig) # Build LUT - delay_measurements =[analysis.measurements[name] for analysis in analyses.values() if name in analysis.measurements] - delay = criterion(delay_measurements) @ PySpice.Unit.u_s - lut_name, meas_path = name.split('__') + delay_measurements = [analysis.measurements[name] for analysis in analyses.values() if name in analysis.measurements] + try: + delay = criterion(delay_measurements) @ PySpice.Unit.u_s + except ValueError as e: + if settings.dry_run: + delay = -1 @ PySpice.Unit.u_s + else: + msg = f'Procedure measure_worst_case_delay_for_path failed for cell {cell.name} ' \ + f'with variation {variation}, pin states {state_map}' + raise ProcedureFailedException(msg) from e + lut_name, *_ = name.split('__') lut_template_size = f'{len(config.parameters["loads"])}x{len(config.parameters["data_slews"])}' lut = LookupTable(lut_name, f'delay_template_{lut_template_size}', total_output_net_capacitance=[load.convert(settings.units.capacitance.prefixed_unit).value], input_net_transition=[data_slew.convert(settings.units.time.prefixed_unit).value]) lut.values[0,0] = delay.convert(settings.units.time.prefixed_unit).value - result.group('pin', output_pin).group('timing', f'/* {input_pin} */').add_group(lut) # FIXME + timing_group.add_group(lut) + result.group('pin', output_pin).add_group(timing_group) return result diff --git a/charlib/characterizer/procedures/combinational/leakage_power.py b/charlib/characterizer/procedures/combinational/leakage_power.py index 639e5670..08a46d65 100644 --- a/charlib/characterizer/procedures/combinational/leakage_power.py +++ b/charlib/characterizer/procedures/combinational/leakage_power.py @@ -64,34 +64,35 @@ def measure_leakage_for_state(cell, config, settings, state_map): temperature=settings.temperature, nominal_temperature=settings.temperature ) - simulation.options('nopage', 'nomod') simulation.operating_point() - try: - analysis = simulator.run(simulation) - except Exception as e: - msg = (f'Procedure measure_leakage_for_state failed for cell {cell.name} ' - f'with state {state_map}') - if settings.debug: - debug_path = settings.debug_dir / cell.name / __name__.split('.')[-1] - debug_path.mkdir(parents=True, exist_ok=True) - with open(debug_path / f'state = {state_map}.sp', 'w', encoding='utf-8') as f: - f.write(str(simulation)) - raise ProcedureFailedException(msg) from e + if settings.debug: + debug_path = settings.debug_dir / cell.name / __name__.split('.')[-1] + debug_path.mkdir(parents=True, exist_ok=True) + with open(debug_path / f'state = {state_map}.sp', 'w', encoding='utf-8') as f: + f.write(str(simulation)) - # Branch current: ngspice names it #branch, simplified to (lower) - i_vdd = float(analysis.branches[settings.primary_power.name.lower()][0]) - power_W = settings.primary_power.voltage * abs(i_vdd) - power_value = (power_W @ PySpice.Unit.u_W).convert( - settings.units.power.prefixed_unit - ).value + if settings.dry_run: + # TODO: Display a message if not settings.quiet + power_value = -1 + else: + try: + analysis = simulator.run(simulation) + except Exception as e: + msg = (f'Procedure measure_leakage_for_state failed for cell {cell.name} ' + f'with state {state_map}') + raise ProcedureFailedException(msg) from e - when_str = build_when_str(state_map) + # Branch current: ngspice names it #branch, simplified to (lower) + i_vdd = float(analysis.branches[settings.primary_power.name.lower()][0]) + power_W = settings.primary_power.voltage * abs(i_vdd) + power_value = (power_W @ PySpice.Unit.u_W).convert( + settings.units.power.prefixed_unit + ).value - # Use when_str as identifier so multiple leakage_power groups in the same cell don't collide result = cell.liberty - lp_group = liberty.Group('leakage_power', f'/* {when_str} */') - lp_group.add_attribute('when', when_str) + lp_group = liberty.Group('leakage_power') + lp_group.add_attribute('when', build_when_str(state_map)) lp_group.add_attribute('value', power_value) result.add_group(lp_group) return result diff --git a/charlib/characterizer/procedures/pin_capacitance/ac_sweep.py b/charlib/characterizer/procedures/pin_capacitance/ac_sweep.py index efa412dc..14f6e8a9 100644 --- a/charlib/characterizer/procedures/pin_capacitance/ac_sweep.py +++ b/charlib/characterizer/procedures/pin_capacitance/ac_sweep.py @@ -78,9 +78,13 @@ def measure_pin_cap_by_ac_sweep(cell, settings, config, target_pin): spice_file.write(str(simulation)) # Measure capacitance as the slope of the conductance with respect to frequency - analysis = simulator.run(simulation) - conductance = np.reciprocal(np.abs(analysis.vin)/i_in) - [*_, capacitance] = np.polynomial.polynomial.polyfit(analysis.frequency, conductance, 1) + if settings.dry_run: + # TODO: Display a message if not settings.quiet + capacitance = -1 + else: + analysis = simulator.run(simulation) + conductance = np.reciprocal(np.abs(analysis.vin)/i_in) + [*_, capacitance] = np.polynomial.polynomial.polyfit(analysis.frequency, conductance, 1) # Add to the liberty group converted_cap = (capacitance @ u_F).convert(settings.units.capacitance.prefixed_unit).value diff --git a/charlib/characterizer/procedures/pin_capacitance/charge_integration.py b/charlib/characterizer/procedures/pin_capacitance/charge_integration.py index 9e15439b..21c48557 100755 --- a/charlib/characterizer/procedures/pin_capacitance/charge_integration.py +++ b/charlib/characterizer/procedures/pin_capacitance/charge_integration.py @@ -26,7 +26,6 @@ def measure_pin_cap_by_charge_integration(cell, settings, config, target_pin): Returns a liberty cell group with the capacitance set on the appropriate pin. """ - result = cell.liberty vdd = settings.primary_power.voltage * settings.units.voltage vss = settings.primary_ground.voltage * settings.units.voltage @@ -95,19 +94,13 @@ def measure_pin_cap_by_charge_integration(cell, settings, config, target_pin): temperature=settings.temperature, nominal_temperature=settings.temperature ) - simulation.options('nopage', 'nomod', post=1, ingold=2) + simulation.options('autostop') # Integrate i(vstim) over each edge; i(vstim) is negative when sourcing current - simulation.measure('tran', 'q_rise', - 'integ i(vstim)', - f'from={t_rise_start:.6g}', - f'to={t_rise_end:.6g}', - run=False) - simulation.measure('tran', 'q_fall', - 'integ i(vstim)', - f'from={t_fall_start:.6g}', - f'to={t_fall_end:.6g}', - run=False) + simulation.measure('tran', 'q_rise', 'integ i(vstim)', + f'from={t_rise_start:.6g}', f'to={t_rise_end:.6g}', run=False) + simulation.measure('tran', 'q_fall', 'integ i(vstim)', + f'from={t_fall_start:.6g}', f'to={t_fall_end:.6g}', run=False) simulation.transient(step_time=t_slew / 10, end_time=t_sim_end, run=False) if settings.debug: @@ -116,23 +109,30 @@ def measure_pin_cap_by_charge_integration(cell, settings, config, target_pin): with open(debug_path / f'{target_pin}.spice', 'w', encoding='utf-8') as spice_file: spice_file.write(str(simulation)) - try: - analysis = simulator.run(simulation) - except Exception as e: - msg = (f'Procedure measure_pin_cap_by_charge_integration failed for cell {cell.name}, ' - f'pin {target_pin}') - raise ProcedureFailedException(msg) from e + if settings.dry_run: + # TODO: Display a message if not settings.quiet + q_rise = -1 + q_fall = -1 + else: + try: + analysis = simulator.run(simulation) + except Exception as e: + msg = (f'Procedure measure_pin_cap_by_charge_integration failed for cell {cell.name}, ' + f'pin {target_pin}') + raise ProcedureFailedException(msg) from e + + q_rise = abs(analysis.measurements.get('q_rise', float('nan'))) + q_fall = abs(analysis.measurements.get('q_fall', float('nan'))) - q_rise = analysis.measurements.get('q_rise', float('nan')) - q_fall = analysis.measurements.get('q_fall', float('nan')) + result = cell.liberty if math.isnan(q_rise) or math.isnan(q_fall): return result # C = |Q| / VDD per edge; capacitance is the worst-case vdd_v = settings.primary_power.voltage - rise_cap_F = abs(q_rise) / vdd_v - fall_cap_F = abs(q_fall) / vdd_v - worst_cap_F = max(rise_cap_F, fall_cap_F) + rise_cap_F = q_rise / vdd_v + fall_cap_F = q_fall / vdd_v + worst_cap_F = max(rise_cap_F, fall_cap_F) def to_lib(cap_F): return (cap_F @ u_F).convert(settings.units.capacitance.prefixed_unit).value diff --git a/charlib/characterizer/procedures/sequential/constraint/metastability/c2q_contour.py b/charlib/characterizer/procedures/sequential/constraint/metastability/c2q_contour.py index d15b4674..8e1a47be 100644 --- a/charlib/characterizer/procedures/sequential/constraint/metastability/c2q_contour.py +++ b/charlib/characterizer/procedures/sequential/constraint/metastability/c2q_contour.py @@ -4,6 +4,7 @@ from charlib.characterizer.procedures import register, ProcedureFailedException from charlib.characterizer import utils, plots +from charlib.liberty import liberty from charlib.liberty.library import LookupTable @register( @@ -224,6 +225,13 @@ def find_setup_hold_for_path(cell, config, settings, variation, path, state_maps filename='contour_sweep_b_setup_as_outer_loop.png', title=_base_title + '\nSweep B: setup outer, hold inner') + # If this is a dry-run, we can't proceed past this point because the next step relies on + # previously measured data + if settings.dry_run: + # TODO: Display a message if not settings.quiet + result_per_state[state_str] = (-1 @ PySpice.Unit.u_s, -1 @ PySpice.Unit.u_s, None) + continue + # Step 6: pick the balanced knee point from the merged contour. boundary_pts = extract_2d_contour(latched_a, latched_b, setup_vals_s, hold_vals_s) (knee_setup_s, knee_hold_s), knee_is_fallback = utils.find_knee_point( @@ -255,7 +263,6 @@ def find_setup_hold_for_path(cell, config, settings, variation, path, state_maps worst_hold = None worst_setup_state = None worst_hold_state = None - for state_str, (setup, hold, _) in result_per_state.items(): if worst_setup is None or setup > worst_setup: worst_setup = setup @@ -291,38 +298,30 @@ def find_setup_hold_for_path(cell, config, settings, variation, path, state_maps # Build liberty output result = cell.liberty - clock_pin = cell.clock - n_ds = len(config.parameters['data_slews']) - n_cs = len(config.parameters['clock_slews']) - lut_template_size = f'{len(config.parameters["clock_slews"])}x{len(config.parameters["data_slews"])}' + lut_size = f'{len(config.parameters["clock_slews"])}x{len(config.parameters["data_slews"])}' + constraint_name = 'rise_constraint' if data_transition == '01' else 'fall_constraint' # Setup timing group - result.group('pin', data_pin).add_group('timing', "/* setup */") - stg = result.group('pin', data_pin).group('timing', "/* setup */") - stg.add_attribute('related_pin', clock_pin.name) - stg.add_attribute('timing_type', 'setup_falling' if clock_pin.is_inverted() else 'setup_rising') - - # Hold timing group - result.group('pin', data_pin).add_group('timing', "/* hold */") - htg = result.group('pin', data_pin).group('timing', "/* hold */") - htg.add_attribute('related_pin', clock_pin.name) - htg.add_attribute('timing_type', 'hold_falling' if clock_pin.is_inverted() else 'hold_rising') - - # add lut to timing group - # rise_constraint when D rises (01), fall_constraint when D falls (10) - cname = 'rise_constraint' if data_transition == '01' else 'fall_constraint' - - setup_lut = LookupTable(cname, f'setup_template_{n_cs}x{n_ds}', + stg = liberty.Group('timing') + stg.add_attribute('related_pin', cell.clock.name) + stg.add_attribute('timing_type', 'setup_falling' if cell.clock.is_inverted() else 'setup_rising') + setup_lut = LookupTable(constraint_name, f'setup_template_{lut_size}', related_pin_transition=[cs.convert(settings.units.time.prefixed_unit).value], - constraint_pin_transition=[ds.convert(settings.units.time.prefixed_unit).value]) + constrained_pin_transition=[ds.convert(settings.units.time.prefixed_unit).value]) setup_lut.values[0, 0] = worst_setup.convert(settings.units.time.prefixed_unit).value stg.add_group(setup_lut) + result.group('pin', data_pin).add_group(stg) - hold_lut = LookupTable(cname, f'hold_template_{n_cs}x{n_ds}', + # Hold timing group + htg = liberty.Group('timing') + htg.add_attribute('related_pin', cell.clock.name) + htg.add_attribute('timing_type', 'hold_falling' if cell.clock.is_inverted() else 'hold_rising') + hold_lut = LookupTable(constraint_name, f'hold_template_{lut_size}', related_pin_transition=[cs.convert(settings.units.time.prefixed_unit).value], - constraint_pin_transition=[ds.convert(settings.units.time.prefixed_unit).value]) + constrained_pin_transition=[ds.convert(settings.units.time.prefixed_unit).value]) hold_lut.values[0, 0] = worst_hold.convert(settings.units.time.prefixed_unit).value htg.add_group(hold_lut) + result.group('pin', data_pin).add_group(htg) return result @@ -551,6 +550,11 @@ def get_t_stabilizing(cell, config, settings, path, state_map, k=2, th_low=0.03, multiplies that by a 'safety factor' k to determine a reasonable stabilizing time.""" simulator, simulation = sim_latch(cell, config, settings, path, state_map, **sim_kwargs) + + if settings.dry_run: + # TODO: Display a message if not settings.quiet + return -1. @ PySpice.Unit.u_s + try: analysis = simulator.run(simulation) except Exception as e: @@ -587,6 +591,10 @@ def get_c2q(cell, config, settings, path, state_map, debug_dir=None, **sim_kwarg # If t_setup + t_hold < 0 fail immediately return float('nan') + if settings.dry_run: + # TODO: Display a message if not settings.quiet + return -1. @ PySpice.Unit.u_s + # Run simulation try: analysis = simulator.run(simulation) diff --git a/charlib/cli/main.py b/charlib/cli/main.py index d3582884..2ac69f03 100644 --- a/charlib/cli/main.py +++ b/charlib/cli/main.py @@ -25,9 +25,6 @@ def main(): parser_compare = subparser.add_parser( 'compare', help='(experimental) Compare two liberty files') - parser_genfunctions = subparser.add_parser( - 'generate_functions', - help='(experimental) Generate YAML maps for registered functions') # Set up charlib run arguments parser_characterize.add_argument( @@ -40,8 +37,8 @@ def main(): '-j', '--jobs', type=int, default=0, help='Specify the number of concurrent jobs') parser_characterize.add_argument( - '--comparewith', type=str, default='', - help='(experimental) A liberty file to compare results with') + '-n', '--no-sim', action='store_true', + help='Perform all tasks except for running simulations') parser_characterize.add_argument( '-f', '--filters', nargs='*', help='A list of one or more regex strings. charlib will only characterize cells matching one or more of the filters.') diff --git a/charlib/cli/run.py b/charlib/cli/run.py index 47120145..192860f2 100755 --- a/charlib/cli/run.py +++ b/charlib/cli/run.py @@ -27,6 +27,7 @@ def run(args): characterizer.settings.debug = characterizer.settings.debug or args.debug characterizer.settings.quiet = characterizer.settings.quiet or args.quiet characterizer.settings.jobs = args.jobs if args.jobs else characterizer.settings.jobs + characterizer.settings.dry_run = characterizer.settings.dry_run or args.no_sim # Filter and add cells if args.filters: @@ -50,7 +51,3 @@ def run(args): f.write(str(liberty)) if not characterizer.settings.quiet: print(f'Results written to {str(libfile.resolve())}') - - # Run any post-characterization analysis - if args.comparewith: - compare(args.comparewith, library) diff --git a/charlib/config/syntax.py b/charlib/config/syntax.py index 2216d5e6..89675003 100644 --- a/charlib/config/syntax.py +++ b/charlib/config/syntax.py @@ -476,6 +476,13 @@ class ConfigFile: 'keyword is set to ``True``' ), default='debug' ) : str, + Optional( + Literal( + 'dry_run', + description='If true, CharLib will perform all steps except for running SPICE ' \ + ' simulations. Equivalent to the ``--no-sim`` command line option.' + ), default=False + ) : bool, Optional( Literal( 'omit_on_failure', diff --git a/charlib/liberty/liberty.py b/charlib/liberty/liberty.py index daf429c4..b727cc0b 100644 --- a/charlib/liberty/liberty.py +++ b/charlib/liberty/liberty.py @@ -1,12 +1,13 @@ """Tools for creating Liberty groups. See Liberty User Guide Vol 1, Chapter 1.""" import re +from abc import ABC, abstractmethod from collections import UserDict INDENT_STR = ' ' -class Statement: +class Statement(ABC): """Abstract base class for Liberty statements, such as Groups and Attributes""" @property def name(self): @@ -22,8 +23,9 @@ def name(self, name): def unique_key(self): return (self.name, self.identifier) + @abstractmethod def to_liberty(self, indent=0, precision=6): - return NotImplemented + pass class Group(Statement):