Skip to content
Closed
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
5 changes: 3 additions & 2 deletions charlib/characterizer/characterizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Comment thread
coderabbitai[bot] marked this conversation as resolved.
futures = [executor.submit(task, *args) for (task, *args) in simulation_tasks]
for future in as_completed(futures):
try:
Expand Down Expand Up @@ -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', {}))
Expand Down
50 changes: 33 additions & 17 deletions charlib/characterizer/procedures/combinational/delay.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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:
Expand All @@ -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
45 changes: 23 additions & 22 deletions charlib/characterizer/procedures/combinational/leakage_power.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 <element_name>#branch, simplified to <element_name> (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 <element_name>#branch, simplified to <element_name> (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
10 changes: 7 additions & 3 deletions charlib/characterizer/procedures/pin_capacitance/ac_sweep.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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

Comment thread
coderabbitai[bot] marked this conversation as resolved.
# 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(
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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)
Expand Down
7 changes: 2 additions & 5 deletions charlib/cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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.')
Expand Down
Loading