diff --git a/gpu_stack/presets/lithography.py b/gpu_stack/presets/lithography.py index 375f3e8..9f82c09 100644 --- a/gpu_stack/presets/lithography.py +++ b/gpu_stack/presets/lithography.py @@ -175,14 +175,10 @@ def _root_assignments(assignments: dict[str, float]) -> dict[str, float]: return assignments -def _source_quark_assignments(protons: int, neutrons: int) -> dict[str, int]: +def _source_nucleon_assignments(protons: int, neutrons: int) -> dict[str, int]: return { - "physical.lithography.source_valence_up_quark_count": ( - 2 * protons + neutrons - ), - "physical.lithography.source_valence_down_quark_count": ( - protons + 2 * neutrons - ), + "physical.lithography.source_proton_count": protons, + "physical.lithography.source_neutron_count": neutrons, } @@ -242,18 +238,20 @@ def asml_euv_public_context_inventory() -> tuple[dict[str, object], ...]: name="source_tin_120_composition_assumption", description=( "Assumption-labeled source-species composition closure for a 120Sn " - "tin plasma source, encoded through exact valence-quark root counts." + "tin plasma source, encoded at the proton-count and neutron-count " + "root layer." ), assignments=_root_assignments( - _source_quark_assignments(protons=50, neutrons=70) + _source_nucleon_assignments(protons=50, neutrons=70) ), source=_NIST_TIN_ATOMIC_DATA_SOURCE, notes=( "This preset says: model the source species as 120Sn for closure. It " "does not say ASML uses isotopically selected 120Sn.", - "The root assignments are exact quark-count bookkeeping from Z=50 " - "and A=120: U=2Z+N and D=Z+2N. Binding, charge state, screening, " - "ionization, plasma temperature, and laser-drive roots remain open.", + "The root assignments are Z=50 and N=70 for tin-120. Valence quark " + "counts U=2Z+N=170 and D=Z+2N=190 are derived by the scope equations " + "from these root values. Binding, charge state, screening, ionization, " + "plasma temperature, and laser-drive roots remain open.", ), ) diff --git a/gpu_stack/presets/materials.py b/gpu_stack/presets/materials.py index 0286df2..3ece030 100644 --- a/gpu_stack/presets/materials.py +++ b/gpu_stack/presets/materials.py @@ -118,29 +118,21 @@ def _root_assignments(assignments: dict[str, int]) -> dict[str, int]: return assignments -def _source_quark_assignments(protons: int, neutrons: int) -> dict[str, int]: +def _source_nucleon_assignments(protons: int, neutrons: int) -> dict[str, int]: return { - "physical.lithography.source_valence_up_quark_count": ( - 2 * protons + neutrons - ), - "physical.lithography.source_valence_down_quark_count": ( - protons + 2 * neutrons - ), + "physical.lithography.source_proton_count": protons, + "physical.lithography.source_neutron_count": neutrons, } -def _medium_component_quark_assignments( +def _medium_component_nucleon_assignments( component: str, protons: int, neutrons: int, ) -> dict[str, int]: return { - f"physical.lithography.medium_component_{component}_valence_up_quark_count": ( - 2 * protons + neutrons - ), - f"physical.lithography.medium_component_{component}_valence_down_quark_count": ( - protons + 2 * neutrons - ), + f"physical.lithography.medium_component_{component}_proton_count": protons, + f"physical.lithography.medium_component_{component}_neutron_count": neutrons, } @@ -148,22 +140,21 @@ def _medium_component_quark_assignments( name="source_hydrogen_1", description=( "Composition-only lithography source isotope preset for hydrogen-1 " - "(protium): one proton and zero neutrons, encoded at the valence " - "up/down quark root layer." + "(protium): one proton and zero neutrons, encoded at the proton-count " + "and neutron-count root layer." ), assignments=_root_assignments( - _source_quark_assignments(protons=1, neutrons=0) + _source_nucleon_assignments(protons=1, neutrons=0) ), source=_provenance( - ( - f"{_NUCLIDE_PROVENANCE} For hydrogen-1: Z=1, A=1, N=0. " - f"{_VALENCE_QUARK_PROVENANCE}" - ), - references=(_IUPAC_NUCLIDE, _PARTICLE_DATA_GROUP_QUARK_MODEL), + f"{_NUCLIDE_PROVENANCE} For hydrogen-1: Z=1, A=1, N=0.", + references=(_IUPAC_NUCLIDE,), ), notes=( "Composition only; does not assign source binding calibration, " "screening, plasma, or optical drive roots.", + "Valence quark counts U=2 and D=1 are derived from Z=1, N=0 by " + "the scope equations.", ), ) @@ -172,22 +163,21 @@ def _medium_component_quark_assignments( name="source_oxygen_16", description=( "Composition-only lithography source isotope preset for oxygen-16: " - "eight protons and eight neutrons, encoded at the valence up/down " - "quark root layer." + "eight protons and eight neutrons, encoded at the proton-count and " + "neutron-count root layer." ), assignments=_root_assignments( - _source_quark_assignments(protons=8, neutrons=8) + _source_nucleon_assignments(protons=8, neutrons=8) ), source=_provenance( - ( - f"{_NUCLIDE_PROVENANCE} For oxygen-16: Z=8, A=16, N=8. " - f"{_VALENCE_QUARK_PROVENANCE}" - ), - references=(_IUPAC_NUCLIDE, _PARTICLE_DATA_GROUP_QUARK_MODEL), + f"{_NUCLIDE_PROVENANCE} For oxygen-16: Z=8, A=16, N=8.", + references=(_IUPAC_NUCLIDE,), ), notes=( "Composition only; does not assign source binding calibration, " "screening, plasma, or optical drive roots.", + "Valence quark counts U=24 and D=24 are derived from Z=8, N=8 by " + "the scope equations.", ), ) @@ -196,24 +186,22 @@ def _medium_component_quark_assignments( name="source_tin_120", description=( "Composition-only EUV lithography source isotope preset for " - "tin-120: fifty protons and seventy neutrons, encoded only at the " - "source valence up/down quark root layer." + "tin-120: fifty protons and seventy neutrons, encoded at the " + "proton-count and neutron-count root layer." ), assignments=_root_assignments( - _source_quark_assignments(protons=50, neutrons=70) + _source_nucleon_assignments(protons=50, neutrons=70) ), source=_provenance( ( "ASML establishes molten tin droplets as the laser-produced " "plasma source material for EUV lithography. CIAAW identifies " "tin as Z=50 and lists tin-120 as a standard isotope with " - "abundance 0.3258(9), so for tin-120 A=120 and N=70. " - f"{_VALENCE_QUARK_PROVENANCE}" + "abundance 0.3258(9), so for tin-120 A=120 and N=70." ), references=( _ASML_EUV_TIN_LPP, _CIAAW_TIN_ISOTOPIC_COMPOSITION, - _PARTICLE_DATA_GROUP_QUARK_MODEL, ), ), notes=( @@ -221,6 +209,8 @@ def _medium_component_quark_assignments( "density, screening, plasma, drive, or optical-response roots.", "Tin-120 is used as a sourced isotope-level stand-in for the EUV " "tin source context, not as a natural-abundance mixture preset.", + "Valence quark counts U=170 and D=190 are derived from Z=50, N=70 " + "by the scope equations.", ), ) @@ -234,22 +224,20 @@ def _medium_component_quark_assignments( assignments=_root_assignments( { "physical.lithography.medium_component_a_stoichiometric_count": 2, - **_medium_component_quark_assignments("a", protons=1, neutrons=0), + **_medium_component_nucleon_assignments("a", protons=1, neutrons=0), "physical.lithography.medium_component_b_stoichiometric_count": 1, - **_medium_component_quark_assignments("b", protons=8, neutrons=8), + **_medium_component_nucleon_assignments("b", protons=8, neutrons=8), } ), source=_provenance( ( f"{_WATER_FORMULA_PROVENANCE} {_NUCLIDE_PROVENANCE} " - "Preset maps H2O to component A=hydrogen-1 and component " - "B=oxygen-16 with stoichiometric counts 2:1; " - f"{_VALENCE_QUARK_PROVENANCE}" + "Preset maps H2O to component A=hydrogen-1 (Z=1, N=0) and " + "component B=oxygen-16 (Z=8, N=8) with stoichiometric counts 2:1." ), references=( _NIST_WEBBOOK_WATER, _IUPAC_NUCLIDE, - _PARTICLE_DATA_GROUP_QUARK_MODEL, ), ), notes=( @@ -259,6 +247,8 @@ def _medium_component_quark_assignments( "Formula-unit proton, neutron, electron, mass, density, and optical " "response values remain derived resolver outputs or explicit " "scenario roots outside this composition preset.", + "Valence quark counts for each component are derived from Z and N " + "by the scope equations.", ), ) diff --git a/gpu_stack/presets/nuclear.py b/gpu_stack/presets/nuclear.py index ea12d27..d1a99bc 100644 --- a/gpu_stack/presets/nuclear.py +++ b/gpu_stack/presets/nuclear.py @@ -217,11 +217,103 @@ def semf_calibration_preset( ).require_source() +_KRANE_SOURCE = ( + "K. S. Krane, Introductory Nuclear Physics, John Wiley & Sons, 1988, " + "Table 3.2: semi-empirical mass formula coefficients. " + "aV = 15.5 MeV, aS = 16.8 MeV, aC = 0.72 MeV, aA = 23 MeV, " + "aP = 34 MeV (pairing coefficient, A^(-1/2) convention)." +) + +_KRANE_PAIRING_SEMANTICS = ( + "Krane's pairing term is delta = +/-aP/sqrt(A) for even-even/odd-odd " + "nuclei; the graph root nuclear_pairing_gap_reference_energy is " + "Delta_pair_ref = aP / sqrt(A_ref) where A_ref is the specific isotope " + "mass number. The caller must supply A_ref to convert aP." +) + +KRANE_SEMF_COEFFICIENTS_MEV = { + "a_vol_mev": 15.5, + "a_surf_mev": 16.8, + "a_coul_mev": 0.72, + "a_asym_mev": 23.0, + "a_pairing_mev": 34.0, +} + +_KRANE_PAIRING_EXPONENT_NOTE = ( + "Krane uses the A^(-1/2) pairing exponent convention. " + "The graph root nuclear_pairing_gap_reference_energy is not directly " + "equal to Krane's aP; it equals aP / sqrt(A_ref) for the reference " + "mass number A_ref of the specific isotope being calibrated." +) + + +def krane_semf_calibration_preset(*, reference_mass_number: float) -> Preset: + """ + Build a sourced SEMF calibration Preset from Krane's Table 3.2 coefficients. + + The four universal coefficients (volume, surface, Coulomb, asymmetry) are + taken directly from Krane (1988). The pairing gap reference energy is + derived as Delta_pair_ref = aP / sqrt(A_ref) where aP = 34 MeV (Krane) and + A_ref is the caller-supplied reference mass number for the specific isotope + being calibrated. + + Parameters + ---------- + reference_mass_number : float + The mass number A of the reference isotope. The pairing gap root will + be set to aP / sqrt(A_ref) in SI joules. + + Returns + ------- + Preset + A Preset with full source provenance, ready for use with the resolver. + """ + if not isinstance(reference_mass_number, (int, float)) or isinstance(reference_mass_number, bool): + raise ValueError("reference_mass_number must be a positive number") + a_ref = float(reference_mass_number) + if not math.isfinite(a_ref) or a_ref <= 0: + raise ValueError("reference_mass_number must be a finite positive number") + + a_vol_j = KRANE_SEMF_COEFFICIENTS_MEV["a_vol_mev"] * MEV_TO_JOULE + a_surf_j = KRANE_SEMF_COEFFICIENTS_MEV["a_surf_mev"] * MEV_TO_JOULE + a_coul_j = KRANE_SEMF_COEFFICIENTS_MEV["a_coul_mev"] * MEV_TO_JOULE + a_asym_j = KRANE_SEMF_COEFFICIENTS_MEV["a_asym_mev"] * MEV_TO_JOULE + a_pair_mev = KRANE_SEMF_COEFFICIENTS_MEV["a_pairing_mev"] + delta_pair_ref_j = (a_pair_mev / math.sqrt(a_ref)) * MEV_TO_JOULE + + return semf_calibration_preset( + name=f"krane_semf_a_ref_{int(a_ref):d}", + description=( + f"SEMF calibration from Krane (1988) Table 3.2, calibrated for " + f"reference mass number A = {int(a_ref)}. Four universal " + "coefficients (aV, aS, aC, aA) plus pairing gap reference energy " + "Delta_pair_ref = aP / sqrt(A_ref)." + ), + assignments={ + "physical.lithography.nuclear_binding_volume_coefficient": a_vol_j, + "physical.lithography.nuclear_binding_surface_coefficient": a_surf_j, + "physical.lithography.nuclear_binding_coulomb_coefficient": a_coul_j, + "physical.lithography.nuclear_binding_asymmetry_coefficient": a_asym_j, + "physical.lithography.nuclear_pairing_gap_reference_energy": delta_pair_ref_j, + }, + source_text=_KRANE_SOURCE, + notes=( + _KRANE_PAIRING_EXPONENT_NOTE, + f"Reference mass number A_ref = {int(a_ref)} was supplied by the caller.", + f"Delta_pair_ref = {a_pair_mev:.1f} / sqrt({int(a_ref)}) " + f"= {a_pair_mev / math.sqrt(a_ref):.6f} MeV " + f"= {delta_pair_ref_j:.6e} J.", + ), + ) + + __all__ = [ + "KRANE_SEMF_COEFFICIENTS_MEV", "MEV_TO_JOULE", "MEV_TO_JOULE_SOURCE", "NUCLEAR_PAIRING_GAP_REFERENCE_ENERGY_ROOT", "SEMF_CALIBRATION_ROOTS", + "krane_semf_calibration_preset", "mev_to_joule", "semf_calibration_root_inventory", "semf_calibration_preset", diff --git a/gpu_stack/scopes/physical_lithography_medium_components.py b/gpu_stack/scopes/physical_lithography_medium_components.py index c834bfb..eca4716 100644 --- a/gpu_stack/scopes/physical_lithography_medium_components.py +++ b/gpu_stack/scopes/physical_lithography_medium_components.py @@ -40,32 +40,26 @@ "LITHOGRAPHY_MEDIUM_COMPOSITION_REF", "lithography_medium_component_a_stoichiometric_count", "lithography_medium_component_b_stoichiometric_count", - "lithography_medium_component_a_valence_up_quark_count", - "lithography_medium_component_a_valence_down_quark_count", - "lithography_medium_component_b_valence_up_quark_count", - "lithography_medium_component_b_valence_down_quark_count", "lithography_medium_component_a_proton_count", "lithography_medium_component_b_proton_count", "lithography_medium_component_a_neutron_count", "lithography_medium_component_b_neutron_count", + "lithography_medium_component_a_valence_up_quark_count", + "lithography_medium_component_a_valence_down_quark_count", + "lithography_medium_component_b_valence_up_quark_count", + "lithography_medium_component_b_valence_down_quark_count", "lithography_medium_component_a_atomic_number", "lithography_medium_component_b_atomic_number", "lithography_medium_component_a_isotope_mass_number", "lithography_medium_component_b_isotope_mass_number", "ineq_lithography_medium_component_a_stoichiometric_count_at_least_one", "ineq_lithography_medium_component_b_stoichiometric_count_at_least_one", - "eq_lithography_medium_component_a_proton_count_from_valence_quarks", - "eq_lithography_medium_component_a_neutron_count_from_valence_quarks", - "eq_lithography_medium_component_b_proton_count_from_valence_quarks", - "eq_lithography_medium_component_b_neutron_count_from_valence_quarks", - "ineq_lithography_medium_component_a_valence_quarks_imply_nonnegative_protons", - "ineq_lithography_medium_component_a_valence_quarks_imply_positive_protons", - "ineq_lithography_medium_component_a_valence_quarks_imply_nonnegative_neutrons", - "eq_lithography_medium_component_a_valence_quark_triplet_integrality", - "ineq_lithography_medium_component_b_valence_quarks_imply_nonnegative_protons", - "ineq_lithography_medium_component_b_valence_quarks_imply_positive_protons", - "ineq_lithography_medium_component_b_valence_quarks_imply_nonnegative_neutrons", - "eq_lithography_medium_component_b_valence_quark_triplet_integrality", + "eq_lithography_medium_component_a_valence_up_quark_count_from_zn", + "eq_lithography_medium_component_a_valence_down_quark_count_from_zn", + "eq_lithography_medium_component_b_valence_up_quark_count_from_zn", + "eq_lithography_medium_component_b_valence_down_quark_count_from_zn", + "ineq_lithography_medium_component_a_proton_count_positive", + "ineq_lithography_medium_component_b_proton_count_positive", "eq_lithography_medium_component_a_atomic_number", "eq_lithography_medium_component_b_atomic_number", "eq_lithography_medium_component_a_isotope_mass_number", diff --git a/gpu_stack/scopes/physical_lithography_medium_components_isotope_relations.py b/gpu_stack/scopes/physical_lithography_medium_components_isotope_relations.py index 97bed12..f062a12 100644 --- a/gpu_stack/scopes/physical_lithography_medium_components_isotope_relations.py +++ b/gpu_stack/scopes/physical_lithography_medium_components_isotope_relations.py @@ -3,11 +3,23 @@ ================================================================== Relations that derive lithography imaging-medium component isotope descriptors. + +The calibration boundary is the proton count Z and neutron count N for each +component. Valence quark counts follow from the proton uud and neutron udd +quark model identities: + + U = 2*Z + N (each proton contributes 2 up quarks, each neutron 1) + D = Z + 2*N (each proton contributes 1 down quark, each neutron 2) + +This decomposition is a real physics identity, not an approximation. It holds +for any nucleus regardless of binding energy or nuclear model. The total quark +count U + D = 3*(Z + N) = 3*A is always divisible by 3 (baryon number +conservation). """ import sympy as sp -from ..core import Approximation, Inequality, RelationRole, eq +from ..core import Approximation, Inequality, Reference, eq from .physical_lithography_medium_components_reference import ( LITHOGRAPHY_MEDIUM_COMPOSITION_REF, ) @@ -27,143 +39,74 @@ ) -eq_lithography_medium_component_a_proton_count_from_valence_quarks = eq( - "physical.eq.lithography_medium_component_a_proton_count_from_valence_quarks", - lithography_medium_component_a_proton_count.symbol, - sp.Rational(1, 3) - * ( - 2 * lithography_medium_component_a_valence_up_quark_count.symbol - - lithography_medium_component_a_valence_down_quark_count.symbol +_QUARK_MODEL_REF = Reference( + citation=( + "Particle Data Group Review of Particle Physics, quark model: " + "proton = uud (2 up, 1 down), neutron = udd (1 up, 2 down). " + "For a nucleus with Z protons and N neutrons: " + "U = 2*Z + N, D = Z + 2*N. " + "PDG, https://pdg.lbl.gov/" ), - "Component A proton count from total valence up/down quark content.", - references=[LITHOGRAPHY_MEDIUM_COMPOSITION_REF], - check_units=True, -) -eq_lithography_medium_component_a_neutron_count_from_valence_quarks = eq( - "physical.eq.lithography_medium_component_a_neutron_count_from_valence_quarks", - lithography_medium_component_a_neutron_count.symbol, - sp.Rational(1, 3) - * ( - 2 * lithography_medium_component_a_valence_down_quark_count.symbol - - lithography_medium_component_a_valence_up_quark_count.symbol - ), - "Component A neutron count from total valence up/down quark content.", - references=[LITHOGRAPHY_MEDIUM_COMPOSITION_REF], - check_units=True, -) -eq_lithography_medium_component_b_proton_count_from_valence_quarks = eq( - "physical.eq.lithography_medium_component_b_proton_count_from_valence_quarks", - lithography_medium_component_b_proton_count.symbol, - sp.Rational(1, 3) - * ( - 2 * lithography_medium_component_b_valence_up_quark_count.symbol - - lithography_medium_component_b_valence_down_quark_count.symbol - ), - "Component B proton count from total valence up/down quark content.", - references=[LITHOGRAPHY_MEDIUM_COMPOSITION_REF], - check_units=True, -) -eq_lithography_medium_component_b_neutron_count_from_valence_quarks = eq( - "physical.eq.lithography_medium_component_b_neutron_count_from_valence_quarks", - lithography_medium_component_b_neutron_count.symbol, - sp.Rational(1, 3) - * ( - 2 * lithography_medium_component_b_valence_down_quark_count.symbol - - lithography_medium_component_b_valence_up_quark_count.symbol - ), - "Component B neutron count from total valence up/down quark content.", - references=[LITHOGRAPHY_MEDIUM_COMPOSITION_REF], - check_units=True, + kind="database", + url="https://pdg.lbl.gov/", ) -ineq_lithography_medium_component_a_valence_quarks_imply_nonnegative_protons = Inequality( - "physical.ineq.lithography_medium_component_a_valence_quarks_imply_nonnegative_protons", - lithography_medium_component_a_valence_down_quark_count.symbol, - 2 * lithography_medium_component_a_valence_up_quark_count.symbol, - "<=", - "Component A valence quark counts must satisfy D <= 2U so the derived proton count is non-negative.", - references=[LITHOGRAPHY_MEDIUM_COMPOSITION_REF], - check_units=True, -) -ineq_lithography_medium_component_a_valence_quarks_imply_positive_protons = Inequality( - "physical.ineq.lithography_medium_component_a_valence_quarks_imply_positive_protons", +eq_lithography_medium_component_a_valence_up_quark_count_from_zn = eq( + "physical.eq.lithography_medium_component_a_valence_up_quark_count_from_zn", lithography_medium_component_a_valence_up_quark_count.symbol, - sp.Rational(1, 2) - * (lithography_medium_component_a_valence_down_quark_count.symbol + sp.Integer(3)), - ">=", - "Component A valence quark counts must satisfy U >= (D + 3)/2 so the derived proton count is at least one.", - references=[LITHOGRAPHY_MEDIUM_COMPOSITION_REF], + 2 * lithography_medium_component_a_proton_count.symbol + + lithography_medium_component_a_neutron_count.symbol, + "Component A up-quark count from proton and neutron counts: U = 2Z + N.", + references=[_QUARK_MODEL_REF], check_units=True, ) -ineq_lithography_medium_component_a_valence_quarks_imply_nonnegative_neutrons = Inequality( - "physical.ineq.lithography_medium_component_a_valence_quarks_imply_nonnegative_neutrons", - lithography_medium_component_a_valence_up_quark_count.symbol, - 2 * lithography_medium_component_a_valence_down_quark_count.symbol, - "<=", - "Component A valence quark counts must satisfy U <= 2D so the derived neutron count is non-negative.", - references=[LITHOGRAPHY_MEDIUM_COMPOSITION_REF], +eq_lithography_medium_component_a_valence_down_quark_count_from_zn = eq( + "physical.eq.lithography_medium_component_a_valence_down_quark_count_from_zn", + lithography_medium_component_a_valence_down_quark_count.symbol, + lithography_medium_component_a_proton_count.symbol + + 2 * lithography_medium_component_a_neutron_count.symbol, + "Component A down-quark count from proton and neutron counts: D = Z + 2N.", + references=[_QUARK_MODEL_REF], check_units=True, ) -eq_lithography_medium_component_a_valence_quark_triplet_integrality = eq( - "physical.eq.lithography_medium_component_a_valence_quark_triplet_integrality", - lithography_medium_component_a_valence_up_quark_count.symbol, - ( - lithography_medium_component_a_valence_up_quark_count.symbol - - sp.Mod( - lithography_medium_component_a_valence_up_quark_count.symbol - + lithography_medium_component_a_valence_down_quark_count.symbol, - 3, - ) - ), - "Total component A valence quark count must be divisible into three-quark baryon triplets.", - references=[LITHOGRAPHY_MEDIUM_COMPOSITION_REF], +eq_lithography_medium_component_b_valence_up_quark_count_from_zn = eq( + "physical.eq.lithography_medium_component_b_valence_up_quark_count_from_zn", + lithography_medium_component_b_valence_up_quark_count.symbol, + 2 * lithography_medium_component_b_proton_count.symbol + + lithography_medium_component_b_neutron_count.symbol, + "Component B up-quark count from proton and neutron counts: U = 2Z + N.", + references=[_QUARK_MODEL_REF], check_units=True, - role=RelationRole.CONSTRAINT, ) -ineq_lithography_medium_component_b_valence_quarks_imply_nonnegative_protons = Inequality( - "physical.ineq.lithography_medium_component_b_valence_quarks_imply_nonnegative_protons", +eq_lithography_medium_component_b_valence_down_quark_count_from_zn = eq( + "physical.eq.lithography_medium_component_b_valence_down_quark_count_from_zn", lithography_medium_component_b_valence_down_quark_count.symbol, - 2 * lithography_medium_component_b_valence_up_quark_count.symbol, - "<=", - "Component B valence quark counts must satisfy D <= 2U so the derived proton count is non-negative.", - references=[LITHOGRAPHY_MEDIUM_COMPOSITION_REF], + lithography_medium_component_b_proton_count.symbol + + 2 * lithography_medium_component_b_neutron_count.symbol, + "Component B down-quark count from proton and neutron counts: D = Z + 2N.", + references=[_QUARK_MODEL_REF], check_units=True, ) -ineq_lithography_medium_component_b_valence_quarks_imply_positive_protons = Inequality( - "physical.ineq.lithography_medium_component_b_valence_quarks_imply_positive_protons", - lithography_medium_component_b_valence_up_quark_count.symbol, - sp.Rational(1, 2) - * (lithography_medium_component_b_valence_down_quark_count.symbol + sp.Integer(3)), + + +ineq_lithography_medium_component_a_proton_count_positive = Inequality( + "physical.ineq.lithography_medium_component_a_proton_count_positive", + lithography_medium_component_a_proton_count.symbol, + sp.Integer(1), ">=", - "Component B valence quark counts must satisfy U >= (D + 3)/2 so the derived proton count is at least one.", - references=[LITHOGRAPHY_MEDIUM_COMPOSITION_REF], - check_units=True, -) -ineq_lithography_medium_component_b_valence_quarks_imply_nonnegative_neutrons = Inequality( - "physical.ineq.lithography_medium_component_b_valence_quarks_imply_nonnegative_neutrons", - lithography_medium_component_b_valence_up_quark_count.symbol, - 2 * lithography_medium_component_b_valence_down_quark_count.symbol, - "<=", - "Component B valence quark counts must satisfy U <= 2D so the derived neutron count is non-negative.", + "Component A must have at least one proton (Z >= 1) to be a nucleus.", references=[LITHOGRAPHY_MEDIUM_COMPOSITION_REF], check_units=True, ) -eq_lithography_medium_component_b_valence_quark_triplet_integrality = eq( - "physical.eq.lithography_medium_component_b_valence_quark_triplet_integrality", - lithography_medium_component_b_valence_up_quark_count.symbol, - ( - lithography_medium_component_b_valence_up_quark_count.symbol - - sp.Mod( - lithography_medium_component_b_valence_up_quark_count.symbol - + lithography_medium_component_b_valence_down_quark_count.symbol, - 3, - ) - ), - "Total component B valence quark count must be divisible into three-quark baryon triplets.", +ineq_lithography_medium_component_b_proton_count_positive = Inequality( + "physical.ineq.lithography_medium_component_b_proton_count_positive", + lithography_medium_component_b_proton_count.symbol, + sp.Integer(1), + ">=", + "Component B must have at least one proton (Z >= 1) to be a nucleus.", references=[LITHOGRAPHY_MEDIUM_COMPOSITION_REF], check_units=True, - role=RelationRole.CONSTRAINT, ) @@ -206,18 +149,12 @@ LITHOGRAPHY_MEDIUM_COMPONENT_ISOTOPE_RELATION_EQUATIONS = [ - eq_lithography_medium_component_a_proton_count_from_valence_quarks, - eq_lithography_medium_component_a_neutron_count_from_valence_quarks, - eq_lithography_medium_component_b_proton_count_from_valence_quarks, - eq_lithography_medium_component_b_neutron_count_from_valence_quarks, - ineq_lithography_medium_component_a_valence_quarks_imply_nonnegative_protons, - ineq_lithography_medium_component_a_valence_quarks_imply_positive_protons, - ineq_lithography_medium_component_a_valence_quarks_imply_nonnegative_neutrons, - eq_lithography_medium_component_a_valence_quark_triplet_integrality, - ineq_lithography_medium_component_b_valence_quarks_imply_nonnegative_protons, - ineq_lithography_medium_component_b_valence_quarks_imply_positive_protons, - ineq_lithography_medium_component_b_valence_quarks_imply_nonnegative_neutrons, - eq_lithography_medium_component_b_valence_quark_triplet_integrality, + eq_lithography_medium_component_a_valence_up_quark_count_from_zn, + eq_lithography_medium_component_a_valence_down_quark_count_from_zn, + eq_lithography_medium_component_b_valence_up_quark_count_from_zn, + eq_lithography_medium_component_b_valence_down_quark_count_from_zn, + ineq_lithography_medium_component_a_proton_count_positive, + ineq_lithography_medium_component_b_proton_count_positive, eq_lithography_medium_component_a_atomic_number, eq_lithography_medium_component_b_atomic_number, eq_lithography_medium_component_a_isotope_mass_number, @@ -226,18 +163,12 @@ LITHOGRAPHY_MEDIUM_COMPONENT_ISOTOPE_RELATION_EXPORTS = [ - "eq_lithography_medium_component_a_proton_count_from_valence_quarks", - "eq_lithography_medium_component_a_neutron_count_from_valence_quarks", - "eq_lithography_medium_component_b_proton_count_from_valence_quarks", - "eq_lithography_medium_component_b_neutron_count_from_valence_quarks", - "ineq_lithography_medium_component_a_valence_quarks_imply_nonnegative_protons", - "ineq_lithography_medium_component_a_valence_quarks_imply_positive_protons", - "ineq_lithography_medium_component_a_valence_quarks_imply_nonnegative_neutrons", - "eq_lithography_medium_component_a_valence_quark_triplet_integrality", - "ineq_lithography_medium_component_b_valence_quarks_imply_nonnegative_protons", - "ineq_lithography_medium_component_b_valence_quarks_imply_positive_protons", - "ineq_lithography_medium_component_b_valence_quarks_imply_nonnegative_neutrons", - "eq_lithography_medium_component_b_valence_quark_triplet_integrality", + "eq_lithography_medium_component_a_valence_up_quark_count_from_zn", + "eq_lithography_medium_component_a_valence_down_quark_count_from_zn", + "eq_lithography_medium_component_b_valence_up_quark_count_from_zn", + "eq_lithography_medium_component_b_valence_down_quark_count_from_zn", + "ineq_lithography_medium_component_a_proton_count_positive", + "ineq_lithography_medium_component_b_proton_count_positive", "eq_lithography_medium_component_a_atomic_number", "eq_lithography_medium_component_b_atomic_number", "eq_lithography_medium_component_a_isotope_mass_number", diff --git a/gpu_stack/scopes/physical_lithography_medium_components_isotope_state.py b/gpu_stack/scopes/physical_lithography_medium_components_isotope_state.py index 689c5b7..d4b2d05 100644 --- a/gpu_stack/scopes/physical_lithography_medium_components_isotope_state.py +++ b/gpu_stack/scopes/physical_lithography_medium_components_isotope_state.py @@ -3,90 +3,100 @@ ============================================================== Isotope composition variables for lithography imaging-medium components. + +The calibration boundary for each component is the proton count Z and +neutron count N. These are the standard isotope identifiers (AME/IUPAC +nuclide notation). Valence quark counts U = 2Z + N and D = Z + 2N are +derived from this basis via the proton uud and neutron udd quark model +identities and are no longer primitive roots. """ import sympy as sp -from ..core import var +from ..core import VariableKind, var from .physical_lithography_medium_components_reference import ( LITHOGRAPHY_MEDIUM_COMPOSITION_REF, ) -lithography_medium_component_a_valence_up_quark_count = var( - "physical.lithography.medium_component_a_valence_up_quark_count", - "N_u_val_A_litho_med", - "count", - "Total valence up-quark count in one component A isotope of the imaging medium.", +lithography_medium_component_a_proton_count = var( + "physical.lithography.medium_component_a_proton_count", "Z_A_litho_med", "count", + "Proton count (atomic number Z) of component A in the imaging-medium formula unit.", scope="physical", integer=True, nonnegative=True, + kind=VariableKind.ROOT_INPUT, sp_units=sp.Integer(1), references=[LITHOGRAPHY_MEDIUM_COMPOSITION_REF], ) -lithography_medium_component_a_valence_down_quark_count = var( - "physical.lithography.medium_component_a_valence_down_quark_count", - "N_d_val_A_litho_med", - "count", - "Total valence down-quark count in one component A isotope of the imaging medium.", +lithography_medium_component_b_proton_count = var( + "physical.lithography.medium_component_b_proton_count", "Z_B_litho_med", "count", + "Proton count (atomic number Z) of component B in the imaging-medium formula unit.", scope="physical", integer=True, nonnegative=True, + kind=VariableKind.ROOT_INPUT, sp_units=sp.Integer(1), references=[LITHOGRAPHY_MEDIUM_COMPOSITION_REF], ) -lithography_medium_component_b_valence_up_quark_count = var( - "physical.lithography.medium_component_b_valence_up_quark_count", - "N_u_val_B_litho_med", - "count", - "Total valence up-quark count in one component B isotope of the imaging medium.", +lithography_medium_component_a_neutron_count = var( + "physical.lithography.medium_component_a_neutron_count", "N_A_litho_med", "count", + "Neutron count N of component A in the imaging-medium formula unit.", scope="physical", integer=True, nonnegative=True, + kind=VariableKind.ROOT_INPUT, sp_units=sp.Integer(1), references=[LITHOGRAPHY_MEDIUM_COMPOSITION_REF], ) -lithography_medium_component_b_valence_down_quark_count = var( - "physical.lithography.medium_component_b_valence_down_quark_count", - "N_d_val_B_litho_med", - "count", - "Total valence down-quark count in one component B isotope of the imaging medium.", +lithography_medium_component_b_neutron_count = var( + "physical.lithography.medium_component_b_neutron_count", "N_B_litho_med", "count", + "Neutron count N of component B in the imaging-medium formula unit.", scope="physical", integer=True, nonnegative=True, + kind=VariableKind.ROOT_INPUT, sp_units=sp.Integer(1), references=[LITHOGRAPHY_MEDIUM_COMPOSITION_REF], ) -lithography_medium_component_a_proton_count = var( - "physical.lithography.medium_component_a_proton_count", "Z_A_litho_med", "count", - "Proton count of component A in the representative imaging-medium formula unit.", +lithography_medium_component_a_valence_up_quark_count = var( + "physical.lithography.medium_component_a_valence_up_quark_count", + "N_u_val_A_litho_med", + "count", + "Total valence up-quark count in one component A isotope of the imaging medium; derived as U = 2Z + N.", scope="physical", integer=True, nonnegative=True, sp_units=sp.Integer(1), references=[LITHOGRAPHY_MEDIUM_COMPOSITION_REF], ) -lithography_medium_component_b_proton_count = var( - "physical.lithography.medium_component_b_proton_count", "Z_B_litho_med", "count", - "Proton count of component B in the representative imaging-medium formula unit.", +lithography_medium_component_a_valence_down_quark_count = var( + "physical.lithography.medium_component_a_valence_down_quark_count", + "N_d_val_A_litho_med", + "count", + "Total valence down-quark count in one component A isotope of the imaging medium; derived as D = Z + 2N.", scope="physical", integer=True, nonnegative=True, sp_units=sp.Integer(1), references=[LITHOGRAPHY_MEDIUM_COMPOSITION_REF], ) -lithography_medium_component_a_neutron_count = var( - "physical.lithography.medium_component_a_neutron_count", "N_A_litho_med", "count", - "Neutron count of component A in the representative imaging-medium formula unit.", +lithography_medium_component_b_valence_up_quark_count = var( + "physical.lithography.medium_component_b_valence_up_quark_count", + "N_u_val_B_litho_med", + "count", + "Total valence up-quark count in one component B isotope of the imaging medium; derived as U = 2Z + N.", scope="physical", integer=True, nonnegative=True, sp_units=sp.Integer(1), references=[LITHOGRAPHY_MEDIUM_COMPOSITION_REF], ) -lithography_medium_component_b_neutron_count = var( - "physical.lithography.medium_component_b_neutron_count", "N_B_litho_med", "count", - "Neutron count of component B in the representative imaging-medium formula unit.", +lithography_medium_component_b_valence_down_quark_count = var( + "physical.lithography.medium_component_b_valence_down_quark_count", + "N_d_val_B_litho_med", + "count", + "Total valence down-quark count in one component B isotope of the imaging medium; derived as D = Z + 2N.", scope="physical", integer=True, nonnegative=True, @@ -132,14 +142,14 @@ LITHOGRAPHY_MEDIUM_COMPONENT_ISOTOPE_STATE_VARIABLES = [ - lithography_medium_component_a_valence_up_quark_count, - lithography_medium_component_a_valence_down_quark_count, - lithography_medium_component_b_valence_up_quark_count, - lithography_medium_component_b_valence_down_quark_count, lithography_medium_component_a_proton_count, lithography_medium_component_b_proton_count, lithography_medium_component_a_neutron_count, lithography_medium_component_b_neutron_count, + lithography_medium_component_a_valence_up_quark_count, + lithography_medium_component_a_valence_down_quark_count, + lithography_medium_component_b_valence_up_quark_count, + lithography_medium_component_b_valence_down_quark_count, lithography_medium_component_a_atomic_number, lithography_medium_component_b_atomic_number, lithography_medium_component_a_isotope_mass_number, @@ -148,14 +158,14 @@ LITHOGRAPHY_MEDIUM_COMPONENT_ISOTOPE_STATE_EXPORTS = [ - "lithography_medium_component_a_valence_up_quark_count", - "lithography_medium_component_a_valence_down_quark_count", - "lithography_medium_component_b_valence_up_quark_count", - "lithography_medium_component_b_valence_down_quark_count", "lithography_medium_component_a_proton_count", "lithography_medium_component_b_proton_count", "lithography_medium_component_a_neutron_count", "lithography_medium_component_b_neutron_count", + "lithography_medium_component_a_valence_up_quark_count", + "lithography_medium_component_a_valence_down_quark_count", + "lithography_medium_component_b_valence_up_quark_count", + "lithography_medium_component_b_valence_down_quark_count", "lithography_medium_component_a_atomic_number", "lithography_medium_component_b_atomic_number", "lithography_medium_component_a_isotope_mass_number", diff --git a/gpu_stack/scopes/physical_lithography_species.py b/gpu_stack/scopes/physical_lithography_species.py index 38e704d..b9e96cb 100644 --- a/gpu_stack/scopes/physical_lithography_species.py +++ b/gpu_stack/scopes/physical_lithography_species.py @@ -4,29 +4,58 @@ Isotope composition variables for lithography source species. -This small layer exposes the source nuclear composition through valence quark -counts, derives proton and neutron counts, then derives the atomic/isotope -descriptors from that composition. +The calibration boundary for the source isotope is the proton count Z and +neutron count N. These are the standard isotope identifiers (AME/IUPAC +nuclide notation). Valence quark counts follow from the proton uud and +neutron udd quark model identities: + + U = 2*Z + N (each proton contributes 2 up quarks, each neutron 1) + D = Z + 2*N (each proton contributes 1 down quark, each neutron 2) + +This decomposition is a real physics identity, not an approximation. Quark +counts are derived from Z and N; they are not primitive roots. """ import sympy as sp -from ..core import Approximation, Inequality, Reference, RelationRole, eq, var +from ..core import Approximation, Inequality, Reference, RelationRole, VariableKind, eq, var LITHOGRAPHY_SOURCE_SPECIES_REF = Reference( citation=( - "Lithography source isotope composition: valence quark content fixes " - "nuclear proton and neutron counts, which then fix atomic number and " - "isotope mass number" + "Lithography source isotope composition: proton count Z and neutron " + "count N fix the isotope identity; valence quark counts U = 2Z + N " + "and D = Z + 2N are derived from the proton (uud) and neutron (udd) " + "quark model identities. PDG, https://pdg.lbl.gov/" ), - kind="memo", + kind="database", + url="https://pdg.lbl.gov/", ) +lithography_source_proton_count = var( + "physical.lithography.source_proton_count", "Z_litho_src", "count", + "Proton count (atomic number Z) of the emitting source isotope.", + scope="physical", + integer=True, + nonnegative=True, + kind=VariableKind.ROOT_INPUT, + sp_units=sp.Integer(1), + references=[LITHOGRAPHY_SOURCE_SPECIES_REF], +) +lithography_source_neutron_count = var( + "physical.lithography.source_neutron_count", "N_nuc_litho_src", "count", + "Neutron count N of the emitting source isotope.", + scope="physical", + integer=True, + nonnegative=True, + kind=VariableKind.ROOT_INPUT, + sp_units=sp.Integer(1), + references=[LITHOGRAPHY_SOURCE_SPECIES_REF], +) lithography_source_valence_up_quark_count = var( "physical.lithography.source_valence_up_quark_count", "N_u_val_litho_src", "count", - "Total valence up-quark count in the emitting source isotope.", + "Total valence up-quark count in the emitting source isotope; derived as U = 2Z + N.", scope="physical", integer=True, positive=True, @@ -35,7 +64,7 @@ ) lithography_source_valence_down_quark_count = var( "physical.lithography.source_valence_down_quark_count", "N_d_val_litho_src", "count", - "Total valence down-quark count in the emitting source isotope.", + "Total valence down-quark count in the emitting source isotope; derived as D = Z + 2N.", scope="physical", integer=True, positive=True, @@ -60,93 +89,32 @@ sp_units=sp.Integer(1), references=[LITHOGRAPHY_SOURCE_SPECIES_REF], ) -lithography_source_proton_count = var( - "physical.lithography.source_proton_count", "Z_litho_src", "count", - "Proton count of the emitting source nucleus or ion species.", - scope="physical", - integer=True, - nonnegative=True, - sp_units=sp.Integer(1), - references=[LITHOGRAPHY_SOURCE_SPECIES_REF], -) -lithography_source_neutron_count = var( - "physical.lithography.source_neutron_count", "N_nuc_litho_src", "count", - "Neutron count of the emitting source isotope.", - scope="physical", - integer=True, - nonnegative=True, - sp_units=sp.Integer(1), - references=[LITHOGRAPHY_SOURCE_SPECIES_REF], -) -eq_lithography_source_proton_count_from_valence_quarks = eq( - "physical.eq.lithography_source_proton_count_from_valence_quarks", - lithography_source_proton_count.symbol, - sp.Rational(1, 3) - * ( - 2 * lithography_source_valence_up_quark_count.symbol - - lithography_source_valence_down_quark_count.symbol - ), - "Proton count from total valence up/down quark content.", - references=[LITHOGRAPHY_SOURCE_SPECIES_REF], - check_units=True, -) -eq_lithography_source_neutron_count_from_valence_quarks = eq( - "physical.eq.lithography_source_neutron_count_from_valence_quarks", - lithography_source_neutron_count.symbol, - sp.Rational(1, 3) - * ( - 2 * lithography_source_valence_down_quark_count.symbol - - lithography_source_valence_up_quark_count.symbol - ), - "Neutron count from total valence up/down quark content.", +eq_lithography_source_valence_up_quark_count_from_zn = eq( + "physical.eq.lithography_source_valence_up_quark_count_from_zn", + lithography_source_valence_up_quark_count.symbol, + 2 * lithography_source_proton_count.symbol + lithography_source_neutron_count.symbol, + "Source up-quark count from proton and neutron counts: U = 2Z + N.", references=[LITHOGRAPHY_SOURCE_SPECIES_REF], check_units=True, ) -ineq_lithography_source_valence_quarks_imply_nonnegative_protons = Inequality( - "physical.ineq.lithography_source_valence_quarks_imply_nonnegative_protons", +eq_lithography_source_valence_down_quark_count_from_zn = eq( + "physical.eq.lithography_source_valence_down_quark_count_from_zn", lithography_source_valence_down_quark_count.symbol, - 2 * lithography_source_valence_up_quark_count.symbol, - "<=", - "Source valence quark counts must satisfy D <= 2U so the derived proton count is non-negative.", + lithography_source_proton_count.symbol + 2 * lithography_source_neutron_count.symbol, + "Source down-quark count from proton and neutron counts: D = Z + 2N.", references=[LITHOGRAPHY_SOURCE_SPECIES_REF], check_units=True, ) -ineq_lithography_source_valence_quarks_imply_positive_protons = Inequality( - "physical.ineq.lithography_source_valence_quarks_imply_positive_protons", - lithography_source_valence_up_quark_count.symbol, - sp.Rational(1, 2) - * (lithography_source_valence_down_quark_count.symbol + sp.Integer(3)), +ineq_lithography_source_proton_count_positive = Inequality( + "physical.ineq.lithography_source_proton_count_positive", + lithography_source_proton_count.symbol, + sp.Integer(1), ">=", - "Source valence quark counts must satisfy U >= (D + 3)/2 so the derived proton count is at least one.", - references=[LITHOGRAPHY_SOURCE_SPECIES_REF], - check_units=True, -) -ineq_lithography_source_valence_quarks_imply_nonnegative_neutrons = Inequality( - "physical.ineq.lithography_source_valence_quarks_imply_nonnegative_neutrons", - lithography_source_valence_up_quark_count.symbol, - 2 * lithography_source_valence_down_quark_count.symbol, - "<=", - "Source valence quark counts must satisfy U <= 2D so the derived neutron count is non-negative.", - references=[LITHOGRAPHY_SOURCE_SPECIES_REF], - check_units=True, -) -eq_lithography_source_valence_quark_triplet_integrality = eq( - "physical.eq.lithography_source_valence_quark_triplet_integrality", - lithography_source_valence_up_quark_count.symbol, - ( - lithography_source_valence_up_quark_count.symbol - - sp.Mod( - lithography_source_valence_up_quark_count.symbol - + lithography_source_valence_down_quark_count.symbol, - 3, - ) - ), - "Total source valence quark count must be divisible into three-quark baryon triplets.", + "Source isotope must have at least one proton (Z >= 1) to be a nucleus.", references=[LITHOGRAPHY_SOURCE_SPECIES_REF], check_units=True, - role=RelationRole.CONSTRAINT, ) eq_lithography_source_atomic_number = eq( "physical.eq.lithography_source_atomic_number", @@ -170,21 +138,18 @@ LITHOGRAPHY_SOURCE_SPECIES_VARIABLES = [ + lithography_source_proton_count, + lithography_source_neutron_count, lithography_source_valence_up_quark_count, lithography_source_valence_down_quark_count, lithography_source_atomic_number, lithography_source_isotope_mass_number, - lithography_source_proton_count, - lithography_source_neutron_count, ] LITHOGRAPHY_SOURCE_SPECIES_EQUATIONS = [ - eq_lithography_source_proton_count_from_valence_quarks, - eq_lithography_source_neutron_count_from_valence_quarks, - ineq_lithography_source_valence_quarks_imply_nonnegative_protons, - ineq_lithography_source_valence_quarks_imply_positive_protons, - ineq_lithography_source_valence_quarks_imply_nonnegative_neutrons, - eq_lithography_source_valence_quark_triplet_integrality, + eq_lithography_source_valence_up_quark_count_from_zn, + eq_lithography_source_valence_down_quark_count_from_zn, + ineq_lithography_source_proton_count_positive, eq_lithography_source_atomic_number, eq_lithography_source_isotope_mass_number, ] @@ -192,18 +157,15 @@ __all__ = [ "LITHOGRAPHY_SOURCE_SPECIES_REF", + "lithography_source_proton_count", + "lithography_source_neutron_count", "lithography_source_valence_up_quark_count", "lithography_source_valence_down_quark_count", "lithography_source_atomic_number", "lithography_source_isotope_mass_number", - "lithography_source_proton_count", - "lithography_source_neutron_count", - "eq_lithography_source_proton_count_from_valence_quarks", - "eq_lithography_source_neutron_count_from_valence_quarks", - "ineq_lithography_source_valence_quarks_imply_nonnegative_protons", - "ineq_lithography_source_valence_quarks_imply_positive_protons", - "ineq_lithography_source_valence_quarks_imply_nonnegative_neutrons", - "eq_lithography_source_valence_quark_triplet_integrality", + "eq_lithography_source_valence_up_quark_count_from_zn", + "eq_lithography_source_valence_down_quark_count_from_zn", + "ineq_lithography_source_proton_count_positive", "eq_lithography_source_atomic_number", "eq_lithography_source_isotope_mass_number", "LITHOGRAPHY_SOURCE_SPECIES_VARIABLES", diff --git a/tests/helpers/lithography.py b/tests/helpers/lithography.py index 33a300b..7e9059f 100644 --- a/tests/helpers/lithography.py +++ b/tests/helpers/lithography.py @@ -8,9 +8,14 @@ def source_quark_assignments(protons, neutrons): + """Return nucleon (proton/neutron) root assignments for the source species. + + Quark counts are now DERIVED from proton/neutron counts via the identity + U = 2Z + N and D = Z + 2N, so proton and neutron counts are the roots. + """ return { - "physical.lithography.source_valence_up_quark_count": 2 * protons + neutrons, - "physical.lithography.source_valence_down_quark_count": protons + 2 * neutrons, + "physical.lithography.source_proton_count": protons, + "physical.lithography.source_neutron_count": neutrons, } diff --git a/tests/test_cli_inventory.py b/tests/test_cli_inventory.py index dc91e6f..b3b4a43 100644 --- a/tests/test_cli_inventory.py +++ b/tests/test_cli_inventory.py @@ -33,7 +33,7 @@ def test_next_work_text_prints_live_compass_sections(): out = buf.getvalue() assert rc == 0 assert "Next work:" in out - assert "graph evidence: variables=1517 equations=959 root_inputs=619" in out + assert "graph evidence: variables=1517 equations=950 root_inputs=619" in out assert "Top 3 highest impact:" in out assert "4 best implementations:" in out assert "10 bugs/risks:" in out diff --git a/tests/test_import_registry.py b/tests/test_import_registry.py index 9fc04d5..dc40ad4 100644 --- a/tests/test_import_registry.py +++ b/tests/test_import_registry.py @@ -14,14 +14,14 @@ "systems": 16, "variables": 1517, "constants": 24, - "equations": 959, + "equations": 950, "root_inputs": 619, - "leaves": 253, + "leaves": 259, "topological_order_length": 1517, "with_sp_units": 1428, "with_references": 1324, - "equations_with_references": 878, - "equations_with_unit_check": 799, + "equations_with_references": 869, + "equations_with_unit_check": 790, "root_kind": 619, "derived_kind": 874, "measured_kind": 0, diff --git a/tests/test_krane_semf_preset.py b/tests/test_krane_semf_preset.py new file mode 100644 index 0000000..20a3d99 --- /dev/null +++ b/tests/test_krane_semf_preset.py @@ -0,0 +1,266 @@ +""" +tests/test_krane_semf_preset.py +================================ + +Verification tests for the Krane SEMF calibration preset factory. + +Checks: + (1) The factory correctly encodes Krane's Table 3.2 coefficients in SI joules. + (2) The resolver produces the expected binding energy for Sn-120 (the EUV tin + source context) within 1% of the experimental value from the AME. + (3) The resolver produces the expected binding energy for Fe-56 within a + justified SEMF tolerance (the liquid-drop formula has known ~1% RMS error + and does not account for magic-number shell corrections). + (4) The factory correctly converts the pairing coefficient as + Delta_pair_ref = aP / sqrt(A_ref). + (5) The preset carries full source provenance. + (6) The factory rejects invalid reference mass numbers. +""" + +import math + +import pytest + +from gpu_stack import Registry, resolve +from gpu_stack.presets import nuclear + + +# --------------------------------------------------------------------------- +# Reference values +# --------------------------------------------------------------------------- + +# Krane (1988) Table 3.2 SEMF coefficients in MeV +KRANE_A_VOL_MEV = 15.5 +KRANE_A_SURF_MEV = 16.8 +KRANE_A_COUL_MEV = 0.72 +KRANE_A_ASYM_MEV = 23.0 +KRANE_A_PAIR_MEV = 34.0 + +# Experimental total binding energies from AME (per-nucleon values) +# Sn-120: B/A = 8.5049 MeV/nucleon, source: AME2020 nuclear mass evaluation +SN120_EXP_BINDING_MEV = 8.5049 * 120 # = 1020.588 MeV +# Fe-56: B/A = 8.7906 MeV/nucleon, source: AME2020 nuclear mass evaluation +FE56_EXP_BINDING_MEV = 8.7906 * 56 # = 492.274 MeV + +# Tolerances: +# Sn-120: Krane SEMF predicts 1020.080 MeV; the ~0.5 MeV error (<0.1%) is +# within the SEMF liquid-drop accuracy for medium-heavy nuclei. +# Fe-56: Krane SEMF predicts 497.741 MeV; the ~5.5 MeV error (~1.1%) reflects +# that the liquid-drop formula does not encode magic-number shell corrections. +# A 2% tolerance accommodates the known SEMF systematic floor. +SN120_TOLERANCE_MEV = 5.0 # <0.5% for a medium-heavy nucleus +FE56_TOLERANCE_MEV = 15.0 # <3% to cover liquid-drop SEMF accuracy floor + + +def _krane_assignments(a_ref: int) -> dict[str, float]: + """Build full SEMF + isotope assignments for use in resolver calls.""" + preset = nuclear.krane_semf_calibration_preset(reference_mass_number=a_ref) + return dict(preset.assignments) + + +# --------------------------------------------------------------------------- +# Coefficient encoding tests +# --------------------------------------------------------------------------- + +class TestKraneSemfCoefficientEncoding: + """Verify that Krane's MeV table values are correctly encoded in SI joules.""" + + def test_volume_coefficient_matches_krane(self): + p = nuclear.krane_semf_calibration_preset(reference_mass_number=120) + assert p.assignments[ + "physical.lithography.nuclear_binding_volume_coefficient" + ] == pytest.approx(KRANE_A_VOL_MEV * nuclear.MEV_TO_JOULE, rel=1e-9) + + def test_surface_coefficient_matches_krane(self): + p = nuclear.krane_semf_calibration_preset(reference_mass_number=120) + assert p.assignments[ + "physical.lithography.nuclear_binding_surface_coefficient" + ] == pytest.approx(KRANE_A_SURF_MEV * nuclear.MEV_TO_JOULE, rel=1e-9) + + def test_coulomb_coefficient_matches_krane(self): + p = nuclear.krane_semf_calibration_preset(reference_mass_number=120) + assert p.assignments[ + "physical.lithography.nuclear_binding_coulomb_coefficient" + ] == pytest.approx(KRANE_A_COUL_MEV * nuclear.MEV_TO_JOULE, rel=1e-9) + + def test_asymmetry_coefficient_matches_krane(self): + p = nuclear.krane_semf_calibration_preset(reference_mass_number=120) + assert p.assignments[ + "physical.lithography.nuclear_binding_asymmetry_coefficient" + ] == pytest.approx(KRANE_A_ASYM_MEV * nuclear.MEV_TO_JOULE, rel=1e-9) + + def test_pairing_reference_energy_for_sn120(self): + # Delta_pair_ref = aP / sqrt(A_ref) for the self-calibrated model + p = nuclear.krane_semf_calibration_preset(reference_mass_number=120) + expected_j = (KRANE_A_PAIR_MEV / math.sqrt(120)) * nuclear.MEV_TO_JOULE + assert p.assignments[ + "physical.lithography.nuclear_pairing_gap_reference_energy" + ] == pytest.approx(expected_j, rel=1e-9) + + def test_pairing_reference_energy_scales_with_reference_mass_number(self): + p56 = nuclear.krane_semf_calibration_preset(reference_mass_number=56) + p120 = nuclear.krane_semf_calibration_preset(reference_mass_number=120) + key = "physical.lithography.nuclear_pairing_gap_reference_energy" + ratio = p56.assignments[key] / p120.assignments[key] + assert ratio == pytest.approx(math.sqrt(120) / math.sqrt(56), rel=1e-9) + + +# --------------------------------------------------------------------------- +# Binding energy resolution tests +# --------------------------------------------------------------------------- + +class TestKraneSemfBindingEnergyResolution: + """ + Verify resolver computes physically reasonable binding energies with Krane + SEMF coefficients. + + Tolerance justification: + - SEMF is a liquid-drop formula: known RMS error ~3 MeV across stable nuclei. + - For Sn-120 (semi-magic Z=50): deviation is small (~0.5 MeV). + - For Fe-56 (doubly-magic-adjacent): liquid-drop gives ~5.5 MeV over-binding + because magic-number shell corrections are not included in SEMF. + """ + + def _source_assignments(self, protons: int, neutrons: int) -> dict[str, object]: + preset = nuclear.krane_semf_calibration_preset( + reference_mass_number=protons + neutrons + ) + return { + **preset.assignments, + "physical.lithography.source_proton_count": protons, + "physical.lithography.source_neutron_count": neutrons, + } + + def test_sn120_binding_energy_resolves_without_missing(self): + assignments = self._source_assignments(protons=50, neutrons=70) + result = resolve( + "physical.lithography.source_nuclear_binding_energy", + assignments=assignments, + ) + assert result.missing == set() + assert not result.violated_constraints + + def test_sn120_binding_energy_within_semf_tolerance(self): + assignments = self._source_assignments(protons=50, neutrons=70) + result = resolve( + "physical.lithography.source_nuclear_binding_energy", + assignments=assignments, + ) + pred_mev = float(result.value) / nuclear.MEV_TO_JOULE + assert abs(pred_mev - SN120_EXP_BINDING_MEV) < SN120_TOLERANCE_MEV, ( + f"Sn-120 SEMF prediction {pred_mev:.3f} MeV deviates more than " + f"{SN120_TOLERANCE_MEV} MeV from experimental {SN120_EXP_BINDING_MEV:.3f} MeV" + ) + + def test_sn120_binding_energy_magnitude_is_physical(self): + assignments = self._source_assignments(protons=50, neutrons=70) + result = resolve( + "physical.lithography.source_nuclear_binding_energy", + assignments=assignments, + ) + pred_mev = float(result.value) / nuclear.MEV_TO_JOULE + # Sn-120 should be around 1020 MeV total binding energy + assert 1000 < pred_mev < 1050, f"Sn-120 binding energy {pred_mev:.1f} MeV outside [1000, 1050] MeV" + + def test_fe56_binding_energy_resolves_without_missing(self): + assignments = self._source_assignments(protons=26, neutrons=30) + result = resolve( + "physical.lithography.source_nuclear_binding_energy", + assignments=assignments, + ) + assert result.missing == set() + assert not result.violated_constraints + + def test_fe56_binding_energy_within_semf_tolerance(self): + assignments = self._source_assignments(protons=26, neutrons=30) + result = resolve( + "physical.lithography.source_nuclear_binding_energy", + assignments=assignments, + ) + pred_mev = float(result.value) / nuclear.MEV_TO_JOULE + assert abs(pred_mev - FE56_EXP_BINDING_MEV) < FE56_TOLERANCE_MEV, ( + f"Fe-56 SEMF prediction {pred_mev:.3f} MeV deviates more than " + f"{FE56_TOLERANCE_MEV} MeV from experimental {FE56_EXP_BINDING_MEV:.3f} MeV" + ) + + def test_fe56_binding_energy_magnitude_is_physical(self): + assignments = self._source_assignments(protons=26, neutrons=30) + result = resolve( + "physical.lithography.source_nuclear_binding_energy", + assignments=assignments, + ) + pred_mev = float(result.value) / nuclear.MEV_TO_JOULE + # Fe-56 should be around 490-510 MeV + assert 480 < pred_mev < 510, f"Fe-56 binding energy {pred_mev:.1f} MeV outside [480, 510] MeV" + + +# --------------------------------------------------------------------------- +# Preset provenance tests +# --------------------------------------------------------------------------- + +class TestKraneSemfPresetProvenance: + """Verify preset carries required source citations.""" + + def test_preset_has_source(self): + p = nuclear.krane_semf_calibration_preset(reference_mass_number=120) + assert p.require_source() is p + + def test_preset_source_cites_krane(self): + p = nuclear.krane_semf_calibration_preset(reference_mass_number=120) + assert "Krane" in p.source + + def test_preset_source_cites_1988(self): + p = nuclear.krane_semf_calibration_preset(reference_mass_number=120) + assert "1988" in p.source + + def test_preset_notes_document_pairing_semantics(self): + p = nuclear.krane_semf_calibration_preset(reference_mass_number=120) + notes_text = " ".join(p.notes) + assert "A^(-1/2)" in notes_text or "A_ref" in notes_text + + def test_preset_assigns_all_semf_roots(self): + p = nuclear.krane_semf_calibration_preset(reference_mass_number=120) + assert set(p.assignments) == set(nuclear.SEMF_CALIBRATION_ROOTS) + + def test_all_assignments_are_positive_joule_values(self): + p = nuclear.krane_semf_calibration_preset(reference_mass_number=120) + for name, value in p.assignments.items(): + assert value > 0, f"{name} assignment is not positive" + assert math.isfinite(value), f"{name} assignment is not finite" + + +# --------------------------------------------------------------------------- +# Factory error handling +# --------------------------------------------------------------------------- + +class TestKraneSemfPresetFactoryErrors: + """Verify the factory rejects invalid inputs.""" + + @pytest.mark.parametrize("bad_a", [0, -1, float("nan"), float("inf"), "120", True]) + def test_factory_rejects_invalid_reference_mass_number(self, bad_a): + with pytest.raises((ValueError, TypeError)): + nuclear.krane_semf_calibration_preset(reference_mass_number=bad_a) + + +# --------------------------------------------------------------------------- +# Module-level export contract +# --------------------------------------------------------------------------- + +def test_krane_semf_coefficients_table_is_exported(): + assert hasattr(nuclear, "KRANE_SEMF_COEFFICIENTS_MEV") + coeffs = nuclear.KRANE_SEMF_COEFFICIENTS_MEV + assert coeffs["a_vol_mev"] == pytest.approx(KRANE_A_VOL_MEV) + assert coeffs["a_surf_mev"] == pytest.approx(KRANE_A_SURF_MEV) + assert coeffs["a_coul_mev"] == pytest.approx(KRANE_A_COUL_MEV) + assert coeffs["a_asym_mev"] == pytest.approx(KRANE_A_ASYM_MEV) + assert coeffs["a_pairing_mev"] == pytest.approx(KRANE_A_PAIR_MEV) + + +def test_krane_factory_is_exported_in_nuclear_all(): + assert "krane_semf_calibration_preset" in nuclear.__all__ + + +def test_nuclear_module_still_does_not_publish_preset_defaults(): + from gpu_stack.core import Preset as PresetClass + exported_presets = [v for v in vars(nuclear).values() if isinstance(v, PresetClass)] + assert exported_presets == [] diff --git a/tests/test_next_work_continuation_contract.py b/tests/test_next_work_continuation_contract.py index 1dd6a10..9d7e94c 100644 --- a/tests/test_next_work_continuation_contract.py +++ b/tests/test_next_work_continuation_contract.py @@ -15,7 +15,7 @@ EXPECTED_GRAPH_EVIDENCE = { "variables": 1517, - "equations": 959, + "equations": 950, "root_inputs": 619, }