Source code for jitxlib.voltage_divider.solver

from dataclasses import dataclass
from typing import List, Optional

from jitx.toleranced import Toleranced
from jitxlib.parts import search_resistors, ExistKeys, DistinctKey
from jitxlib.parts._types.main import to_component, PartJSON
from jitxlib.parts._types.component import MinMax
from jitxlib.parts._types.resistor import Resistor

from .constraints import VoltageDividerConstraints
from .errors import (
    NoPrecisionSatisfiesConstraintsError,
    VinRangeTooLargeError,
    IncompatibleVinVoutError,
    NoSolutionFoundError,
)


[docs] @dataclass class VoltageDividerSolution: """ Voltage Divider Solution Type """ R_h: Resistor R_l: Resistor vo: Toleranced
[docs] @dataclass class Ratio: high: float low: float loss: float
[docs] def solve(constraints: VoltageDividerConstraints) -> VoltageDividerSolution: """ Solve the Voltage Divider Constraint Problem. """ search_prec = constraints.search_range goals = constraints.compute_initial_guess() for g in goals: if g < 0.0: raise IncompatibleVinVoutError(constraints.v_in, constraints.v_out) goal_r_hi, goal_r_lo = goals # Screen the input voltage requirement with perfect resistors vin_screen = constraints.compute_objective( Toleranced.exact(goal_r_hi), Toleranced.exact(goal_r_lo) ) if not constraints.is_compliant(vin_screen): raise VinRangeTooLargeError(goals, vin_screen) # Pre-screen precision series pre_screen = [] for std_prec in constraints.prec_series: vo = constraints.compute_objective( Toleranced.percent(goal_r_hi, std_prec), Toleranced.percent(goal_r_lo, std_prec), ) pre_screen.append((constraints.is_compliant(vo), std_prec, vo)) first_valid_series = next((i for i, elem in enumerate(pre_screen) if elem[0]), None) if first_valid_series is not None: series = constraints.prec_series[first_valid_series:] else: raise NoPrecisionSatisfiesConstraintsError(goals, pre_screen) # Try to solve for each valid precision for std_prec in series: print(f"-> Precision {std_prec}%") sol = solve_over_series(constraints, std_prec, search_prec) if sol is not None: return sol raise NoSolutionFoundError( "Failed to Source Resistors to Satisfy Voltage Divider Constraints" )
[docs] def solve_over_series( constraints: VoltageDividerConstraints, precision: float, search_prec: float ) -> Optional[VoltageDividerSolution]: goal_r_hi, goal_r_lo = constraints.compute_initial_guess() hi_res = query_resistance_by_values(constraints, goal_r_hi, precision, search_prec) lo_res = query_resistance_by_values(constraints, goal_r_lo, precision, search_prec) for ratio in sort_pairs_by_best_fit(constraints, precision, hi_res, lo_res): sol = filter_query_results(constraints, ratio, precision) if sol is not None: return sol return None
[docs] def filter_query_results( constraints: VoltageDividerConstraints, ratio: Ratio, precision: float ) -> Optional[VoltageDividerSolution]: print(f" - Querying resistors for R-h={ratio.high}Ω R-l={ratio.low}Ω") r_his = query_resistors(constraints, ratio.high, precision) r_los = query_resistors(constraints, ratio.low, precision) min_srcs = constraints.min_sources if len(r_his) < min_srcs or len(r_los) < min_srcs: print( f" Ignoring: there must be at least {min_srcs} resistors of each type" ) return None r_hi_cmp = r_his[0] r_lo_cmp = r_los[0] vo_set = study_solution(constraints, r_hi_cmp, r_lo_cmp, constraints.temp_range) vo_valids = [constraints.is_compliant(vo) for vo in vo_set] is_valid = all(vo_valids) if not is_valid: print(" Ignoring: not a solution when taking into account TCRs.") def fmt(ok, vo): return "OK" if ok else f"FAIL ({vo} V)" print(f" min-temp: {fmt(vo_valids[0], vo_set[0])}") print(f" max-temp: {fmt(vo_valids[1], vo_set[1])}") return None # TODO: Compute the worst case v-out here and use that instead of just the first worst_case_vo = vo_set[0] # Print solution found mpn1 = r_hi_cmp.mpn mpn2 = r_lo_cmp.mpn vout_str = f"({vo_set[0]}, {vo_set[1]})V" if len(vo_set) > 1 else f"({vo_set[0]})V" try: current = vo_set[0].typ / ratio.low except Exception: current = "unknown" print( f" Solved: mpn1={mpn1}, mpn2={mpn2}, v-out={vout_str}, current={current}A" ) return VoltageDividerSolution(r_hi_cmp, r_lo_cmp, worst_case_vo)
[docs] def sort_pairs_by_best_fit( constraints: VoltageDividerConstraints, precision: float, hi_res: List[float], lo_res: List[float], ) -> List[Ratio]: ratios = [] for rh in hi_res: for rl in lo_res: loss = constraints.compute_loss(rh, rl, precision) if loss is not None: ratios.append(Ratio(rh, rl, loss)) ratios.sort(key=lambda r: r.loss) return ratios
[docs] def query_resistance_by_values( constraints: VoltageDividerConstraints, goal_r: float, r_prec: float, min_prec: float, ) -> List[float]: """ Query for resistance values within the specified precision range using search_resistors. Returns a list of resistance values (float). """ def to_float(r: PartJSON) -> float: if not isinstance(r, int | float): raise ValueError( f"Expected returned resistance value from database to be an int|float, got {type(r)}: {r}" ) return float(r) # Use search_resistors with distinct resistance exist_keys = ExistKeys(["tcr_pos", "tcr_neg"]) distinct_key = DistinctKey("resistance") base_query = constraints.base_query resistances = search_resistors( base_query, resistance=Toleranced.percent(goal_r, min_prec), precision=r_prec / 100.0, exist=exist_keys, distinct=distinct_key, ) # Case from int to float (mimic stanza codebase, the database is sensitive to the difference, maybe due to caching). return [to_float(r) for r in resistances]
[docs] def query_resistors( constraints: VoltageDividerConstraints, target: float, prec: float ) -> List[Resistor]: """ Query for resistors matching a particular target resistance and precision. Returns a list of Resistor objects. """ def to_resistor(r: PartJSON) -> Resistor: c = to_component(r) if not isinstance(c, Resistor): raise ValueError(f"Expected Resistor, got {type(c)}: {c}") return c exist_keys = ExistKeys(["tcr_pos", "tcr_neg"]) base_query = constraints.base_query results = search_resistors( base_query, resistance=target, precision=prec / 100.0, exist=exist_keys, limit=constraints.min_sources, ) # Convert results to Resistor objects return [to_resistor(r) for r in results]
[docs] def study_solution( constraints: VoltageDividerConstraints, r_hi: Resistor, r_lo: Resistor, temp_range: Toleranced, ) -> List[Toleranced]: """ Compute the voltage divider expected output over a temperature range. Returns a list of Toleranced values for [min_temp, max_temp]. """ if r_lo.resistance == 0.0 and r_hi.resistance == 0.0: raise ValueError( f"Can't check output voltage current for a solution with two zero ohm resistors {r_lo.mpn} and {r_hi.mpn}." ) # Compute TCR deviations for min and max temperature lo_drs = [ compute_tcr_deviation(r_lo, temp_range.min_value), compute_tcr_deviation(r_lo, temp_range.max_value), ] hi_drs = [ compute_tcr_deviation(r_hi, temp_range.min_value), compute_tcr_deviation(r_hi, temp_range.max_value), ] r_lo_val = get_resistance(r_lo) r_hi_val = get_resistance(r_hi) results = [] for lo_dr, hi_dr in zip(lo_drs, hi_drs, strict=True): if lo_dr is not None and hi_dr is not None: vout = constraints.compute_objective(r_hi_val, r_lo_val, hi_dr, lo_dr) results.append(vout) else: raise ValueError("No TCR Data") return results
[docs] def get_resistance(r: Resistor) -> Toleranced: """ Get the resistance value as a Toleranced. Uses the internal information of the Resistor component object to construct the resistance value with tolerances. Raises an error if tolerance is None. Always expects MinMax for tolerance. """ if r.tolerance is None: raise ValueError( "Resistor tolerance must be specified (MinMax). None is not allowed." ) return tol_minmax(r.resistance, r.tolerance)
[docs] def tol_minmax(typ: float, tolerance: MinMax) -> Toleranced: """ Create a Toleranced value from the MinMax range. Mirrors the Stanza implementation: tol(v, tolerance:MinMaxRange): coeff = min-max(1.0 + min(tolerance), 1.0 + max(tolerance)) v * coeff """ coeff = Toleranced.min_max(1.0 + tolerance.min, 1.0 + tolerance.max) return typ * coeff
[docs] def compute_tcr_deviation( resistor: Resistor, temperature: float ) -> Optional[Toleranced]: """ Compute the expected deviation window of a given resistor at a given temperature. This function mirrors the Stanza implementation in component-types.stanza: - Extracts tcr and reference temperature from the resistor. - Converts pos/neg to a Toleranced interval using Toleranced.min_max. - Calls compute_tcr_deviation_interval. - Returns None if tcr is not present. NOTE: This includes a workaround for known database issues with TCR values, as described in the Stanza code and PROD-328. """ tcr = resistor.tcr ref_temp = 25.0 # Default reference temperature if tcr is None: return None # This mirrors the Stanza hack for database issues: # See: https://linear.app/jitx/issue/PROD-328/tcr-values-in-database-seem-wrong p, n = tcr.pos, tcr.neg tcr_interval = Toleranced.min_max(min(p, n), max(p, n)) return compute_tcr_deviation_interval(tcr_interval, temperature, ref_temp)
[docs] def compute_tcr_deviation_interval( tcr: Toleranced, temperature: float, ref_temp: float = 25.0 ) -> Toleranced: """ Compute the expected deviation window of a given temperature coefficient. This function mirrors the Stanza implementation: - Returns 1.0 + (diff * tcr), where diff = temperature - ref_temp. - The result is a Toleranced window for the deviation (typically ~0.9 to 1.1). """ diff = temperature - ref_temp return 1.0 + (diff * tcr)