chialoops 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- chia/analysis/__init__.py +1 -0
- chia/analysis/sustainability/__init__.py +23 -0
- chia/analysis/sustainability/focal.py +472 -0
- chia/analysis/sustainability/pfas.py +127 -0
- chia/aws/__init__.py +0 -0
- chia/aws/config.py +98 -0
- chia/aws/ec2.py +333 -0
- chia/aws/host.py +76 -0
- chia/aws/s3.py +350 -0
- chia/aws/test/test_s3.py +366 -0
- chia/aws/test/test_s3_e2e.py +153 -0
- chia/base/ChiaFunction.py +711 -0
- chia/base/bypass.py +442 -0
- chia/base/cache.py +430 -0
- chia/base/chia_kv_store.py +199 -0
- chia/base/chia_wait.py +331 -0
- chia/base/colocated.py +204 -0
- chia/base/dispatch_proxy.py +247 -0
- chia/base/llm_call.py +93 -0
- chia/base/pid_registry.py +471 -0
- chia/base/test/ChiaFunction_tb.py +272 -0
- chia/base/test/chia_wait_tb.py +186 -0
- chia/base/test/pid_registry_tb.py +743 -0
- chia/base/test/ray_job_stop_helper.py +76 -0
- chia/base/test/sigterm_cleanup_helper.py +74 -0
- chia/base/test/test_bypass_tags.py +479 -0
- chia/base/test/test_cache_tags.py +520 -0
- chia/base/test/test_chia_remote_blocking.py +140 -0
- chia/base/test/test_colocated_live.py +264 -0
- chia/base/test/test_dispatch_proxy_live.py +76 -0
- chia/base/tools/AsyncBashTool.py +98 -0
- chia/base/tools/AsyncJobTool.py +119 -0
- chia/base/tools/BashTool.py +79 -0
- chia/base/tools/ChiaTool.py +378 -0
- chia/base/tools/ChiaToolTemplate.py +62 -0
- chia/base/tools/test/test_tool.py +395 -0
- chia/base/tools/util.py +25 -0
- chia/chipyard/__init__.py +0 -0
- chia/chipyard/chipyard_hammer.py +253 -0
- chia/chipyard/chisel_build_node.py +540 -0
- chia/chipyard/circt.py +834 -0
- chia/chipyard/cosim_node.py +100 -0
- chia/chipyard/firemarshal_node.py +358 -0
- chia/chipyard/macrocompiler.py +371 -0
- chia/chipyard/numa_prefix.py +86 -0
- chia/chipyard/riscv_build_node.py +204 -0
- chia/chipyard/riscv_dv_gen_node.py +381 -0
- chia/chipyard/riscv_objdump_node.py +164 -0
- chia/chipyard/spike_build_node.py +299 -0
- chia/chipyard/state_def.py +456 -0
- chia/chipyard/test/cluster/macrocompiler_e2e_test.py +351 -0
- chia/chipyard/test/riscv_dv_gen_e2e_driver.py +105 -0
- chia/chipyard/test/spike_cosim_e2e_driver.py +71 -0
- chia/chipyard/test/test_build_program.py +43 -0
- chia/chipyard/test/test_chipyard_hammer_live.py +319 -0
- chia/chipyard/test/test_chisel_riscv_verilator_e2e.py +223 -0
- chia/chipyard/test/test_firemarshal_node.py +105 -0
- chia/chipyard/test/test_firesim_metasim_run.py +112 -0
- chia/chipyard/test/test_riscv_build_failure.py +88 -0
- chia/chipyard/test/test_riscv_timeout_output_decode.py +45 -0
- chia/chipyard/test/test_spike_build_node.py +164 -0
- chia/chipyard/test/test_spike_golden_model.py +110 -0
- chia/chipyard/test/torture_e2e_driver.py +180 -0
- chia/chipyard/test/torture_run_node_tb.py +182 -0
- chia/chipyard/torture_run_node.py +407 -0
- chia/chipyard/verilator_run_node.py +721 -0
- chia/cli/__init__.py +0 -0
- chia/cli/down.py +143 -0
- chia/cli/firesim_cmds.py +297 -0
- chia/cli/job.py +81 -0
- chia/cli/main.py +276 -0
- chia/cli/ray_passthrough.py +58 -0
- chia/cli/up.py +481 -0
- chia/cli/viz_cmd.py +29 -0
- chia/cli/viz_profile_cmd.py +55 -0
- chia/cluster/__init__.py +0 -0
- chia/cluster/aws_nodes.py +784 -0
- chia/cluster/config.py +1013 -0
- chia/cluster/docker.py +136 -0
- chia/cluster/gcp_nodes.py +762 -0
- chia/cluster/log.py +45 -0
- chia/cluster/node_setup.py +1200 -0
- chia/cluster/ray_stop.py +450 -0
- chia/cluster/ssh.py +239 -0
- chia/cluster/tailnet.py +548 -0
- chia/cluster/test/down/test_scoped_down_live.py +206 -0
- chia/cluster/test/multi_cluster/test_multi_cluster_add.py +136 -0
- chia/cluster/test/tailnet/__init__.py +0 -0
- chia/cluster/test/tailnet/test_tailnet.py +692 -0
- chia/cluster/test/tunnel/test_aws_nodes.py +227 -0
- chia/cluster/test/tunnel/test_cross_machine.py +393 -0
- chia/cluster/test/tunnel/test_dispatch_proxy.py +177 -0
- chia/cluster/test/tunnel/test_ec2_cluster.py +55 -0
- chia/cluster/test/tunnel/test_ec2_concurrency.py +231 -0
- chia/cluster/test/tunnel/test_ec2_tool.py +400 -0
- chia/cluster/test/tunnel/test_gcp_cluster.py +60 -0
- chia/cluster/test/tunnel/test_gcp_nodes.py +492 -0
- chia/cluster/tunnel.py +281 -0
- chia/cluster/worker_provisioner.py +1141 -0
- chia/database/__init__.py +25 -0
- chia/database/base.py +334 -0
- chia/database/postgres_node.py +715 -0
- chia/database/sqlite_node.py +672 -0
- chia/database/test/test_postgres_node_live.py +527 -0
- chia/database/test/test_sqlite_node_live.py +510 -0
- chia/esp/__init__.py +0 -0
- chia/esp/esp_workspace.py +1111 -0
- chia/esp/state_def.py +203 -0
- chia/esp/test/cluster/accel_e2e_test.py +156 -0
- chia/esp/test/cluster/accel_loop_e2e_test.py +150 -0
- chia/esp/test/cluster/fpga_e2e_test.py +168 -0
- chia/esp/test/cluster/hls_loop_e2e_test.py +161 -0
- chia/esp/test/cluster/soft_build_e2e_test.py +149 -0
- chia/esp/test/cluster/xmsim_e2e_test.py +112 -0
- chia/esp/test/test_esp_local.py +607 -0
- chia/firesim/__init__.py +0 -0
- chia/firesim/build_node.py +1942 -0
- chia/firesim/chia_functions.py +162 -0
- chia/firesim/config.py +334 -0
- chia/firesim/run_node.py +788 -0
- chia/firesim/spec_parser.py +131 -0
- chia/firesim/state_def.py +81 -0
- chia/firesim/suite_runner.py +477 -0
- chia/firesim/test/test_firesim_build_e2e.py +96 -0
- chia/firesim/test/test_firesim_build_megaboom_all.py +115 -0
- chia/firesim/test/test_firesim_build_ray.py +129 -0
- chia/firesim/test/test_firesim_metasim_build.py +84 -0
- chia/firesim/test/test_firesim_run_e2e.py +116 -0
- chia/firesim/test/test_firesim_suite_e2e.py +99 -0
- chia/firesim/workloads.py +310 -0
- chia/github/__init__.py +0 -0
- chia/github/github_client.py +192 -0
- chia/github/github_issues_node.py +226 -0
- chia/github/github_pulls_node.py +269 -0
- chia/github/state_def.py +205 -0
- chia/github/test/test_github_issues_e2e.py +149 -0
- chia/models/antigravity.py +757 -0
- chia/models/bedrock.py +612 -0
- chia/models/claude.py +1411 -0
- chia/models/codex.py +738 -0
- chia/models/copilot.py +734 -0
- chia/models/ollama.py +60 -0
- chia/models/openai_compat.py +728 -0
- chia/models/openai_providers.py +97 -0
- chia/models/opencode.py +889 -0
- chia/models/tests/cluster/ollama_e2e_test.py +294 -0
- chia/models/tests/cluster/ollama_tool_hammer.py +132 -0
- chia/models/tests/cluster/vllm_e2e_test.py +196 -0
- chia/models/tests/conftest.py +98 -0
- chia/models/tests/fixtures/bedrock/capture_real_fixtures.py +68 -0
- chia/models/tests/fixtures/make_synthetic_fixtures.py +87 -0
- chia/models/tests/fixtures/vertex/capture_real_fixtures.py +72 -0
- chia/models/tests/test_antigravity.py +477 -0
- chia/models/tests/test_bedrock.py +678 -0
- chia/models/tests/test_claude_api.py +1025 -0
- chia/models/tests/test_codex.py +458 -0
- chia/models/tests/test_copilot.py +530 -0
- chia/models/tests/test_ollama.py +317 -0
- chia/models/tests/test_openai_compat.py +996 -0
- chia/models/tests/test_openai_providers.py +629 -0
- chia/models/tests/test_opencode.py +1119 -0
- chia/models/tests/test_vertex.py +908 -0
- chia/models/tests/test_vllm.py +317 -0
- chia/models/vertex.py +757 -0
- chia/models/vllm.py +75 -0
- chia/simulators/__init__.py +43 -0
- chia/simulators/champsim.py +1152 -0
- chia/simulators/gem5.py +1216 -0
- chia/simulators/tests/__init__.py +0 -0
- chia/simulators/tests/test_champsim_live.py +474 -0
- chia/simulators/tests/test_champsim_smoke_trace.py +138 -0
- chia/simulators/tests/test_gem5_live.py +658 -0
- chia/simulators/tests/traces/generate_smoke_trace.py +66 -0
- chia/trace/__init__.py +11 -0
- chia/trace/metrics.py +158 -0
- chia/trace/profile_table.py +163 -0
- chia/trace/profile_template.html +777 -0
- chia/trace/profile_viz.py +549 -0
- chia/trace/profile_viz_html.py +181 -0
- chia/trace/profiler.py +563 -0
- chia/trace/test/test_metrics_logger.py +114 -0
- chia/trace/test/test_profiler.py +597 -0
- chia/trace/viz.py +733 -0
- chia/vlsi/__init__.py +4 -0
- chia/vlsi/hammer.py +396 -0
- chia/vlsi/sram_cacti/__init__.py +0 -0
- chia/vlsi/sram_cacti/cacti_macrocompiler.py +18 -0
- chia/vlsi/sram_cacti/cacti_runner.py +218 -0
- chia/vlsi/sram_cacti/cluster/cacti_smoke_test.py +360 -0
- chia/vlsi/sram_cacti/lef_gen.py +189 -0
- chia/vlsi/sram_cacti/liberty_gen.py +290 -0
- chia/vlsi/sram_cacti/sram_characterize.py +217 -0
- chia/vlsi/sram_cacti/test/conftest.py +80 -0
- chia/vlsi/sram_cacti/test/test_cacti_macrocompiler.py +46 -0
- chia/vlsi/sram_cacti/test/test_cacti_runner.py +122 -0
- chia/vlsi/sram_cacti/test/test_file_validation.py +206 -0
- chia/vlsi/sram_cacti/test/test_integration.py +71 -0
- chia/vlsi/sram_cacti/test/test_lef_gen.py +64 -0
- chia/vlsi/sram_cacti/test/test_liberty_gen.py +85 -0
- chia/vlsi/sram_cacti/test/test_sram_characterize.py +129 -0
- chia/vlsi/sram_cacti/test_nport.py +434 -0
- chia/vlsi/tests/__init__.py +0 -0
- chia/vlsi/tests/test_hammer_live.py +549 -0
- chialoops-1.0.0.dist-info/METADATA +149 -0
- chialoops-1.0.0.dist-info/RECORD +209 -0
- chialoops-1.0.0.dist-info/WHEEL +5 -0
- chialoops-1.0.0.dist-info/entry_points.txt +2 -0
- chialoops-1.0.0.dist-info/licenses/LICENSE +29 -0
- chialoops-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Post-design analysis utilities (carbon/sustainability modeling, etc.)."""
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from chia.analysis.sustainability.focal import (
|
|
2
|
+
ALPHA_EMBODIED_DOMINATED,
|
|
3
|
+
ALPHA_OPERATIONAL_DOMINATED,
|
|
4
|
+
SWEEPABLE_PARAMETERS,
|
|
5
|
+
FocalBothRefs,
|
|
6
|
+
FocalComparison,
|
|
7
|
+
FocalSweep,
|
|
8
|
+
focal_compare,
|
|
9
|
+
focal_sweep,
|
|
10
|
+
sweep_both_arches_as_ref,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"ALPHA_EMBODIED_DOMINATED",
|
|
15
|
+
"ALPHA_OPERATIONAL_DOMINATED",
|
|
16
|
+
"SWEEPABLE_PARAMETERS",
|
|
17
|
+
"FocalBothRefs",
|
|
18
|
+
"FocalComparison",
|
|
19
|
+
"FocalSweep",
|
|
20
|
+
"focal_compare",
|
|
21
|
+
"focal_sweep",
|
|
22
|
+
"sweep_both_arches_as_ref",
|
|
23
|
+
]
|
|
@@ -0,0 +1,472 @@
|
|
|
1
|
+
"""FOCAL: a first-order carbon model for comparing processor architectures.
|
|
2
|
+
|
|
3
|
+
Implements the Normalized Carbon Footprint (NCF) metric from Lieven Eeckhout,
|
|
4
|
+
"FOCAL: A First-Order Carbon Model to Assess Processor Sustainability"
|
|
5
|
+
(ASPLOS '24, https://doi.org/10.1145/3620665.3640415).
|
|
6
|
+
|
|
7
|
+
FOCAL weighs a design's *embodied* footprint (proxied by chip area) against its
|
|
8
|
+
*operational* footprint (proxied by power under a fixed-time use case, or by
|
|
9
|
+
energy under a fixed-work use case) using a single embodied-to-operational
|
|
10
|
+
weight ``alpha`` in [0, 1]. Comparing a ``test`` design against a ``ref``
|
|
11
|
+
baseline, the fixed-time NCF is::
|
|
12
|
+
|
|
13
|
+
NCF(test, ref) = alpha * (A_test / A_ref) + (1 - alpha) * (P_test / P_ref)
|
|
14
|
+
|
|
15
|
+
where ``A`` is chip area and ``P`` is power consumption. ``NCF < 1`` means the
|
|
16
|
+
test design has a lower total carbon footprint than the reference (i.e. it is
|
|
17
|
+
more sustainable); ``NCF > 1`` means it is worse; ``NCF == 1`` means the two are
|
|
18
|
+
carbon-equal. The same expression holds for the fixed-work scenario with energy
|
|
19
|
+
substituted for power.
|
|
20
|
+
|
|
21
|
+
``alpha`` is the embodied-to-operational weight in [0, 1]. Paper scenarios:
|
|
22
|
+
``alpha = 0.8`` (embodied-dominated, e.g. mobile and datacenter hardware) and
|
|
23
|
+
``alpha = 0.2`` (operational-dominated, e.g. always-connected devices).
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import sys
|
|
29
|
+
import textwrap
|
|
30
|
+
import warnings
|
|
31
|
+
from dataclasses import dataclass
|
|
32
|
+
from pathlib import Path
|
|
33
|
+
from typing import Sequence
|
|
34
|
+
|
|
35
|
+
# Embodied-to-operational weight scenarios from the FOCAL paper (Section 5).
|
|
36
|
+
ALPHA_EMBODIED_DOMINATED = 0.8 # e.g. mobile / datacenter hardware
|
|
37
|
+
ALPHA_OPERATIONAL_DOMINATED = 0.2 # e.g. always-connected devices
|
|
38
|
+
|
|
39
|
+
# The five FOCAL parameters that focal_sweep can vary.
|
|
40
|
+
SWEEPABLE_PARAMETERS = ("test_pwr", "ref_pwr", "test_area", "ref_area", "alpha_ref")
|
|
41
|
+
|
|
42
|
+
_PARAM_LABELS = {
|
|
43
|
+
"test_pwr": "test power",
|
|
44
|
+
"ref_pwr": "ref power",
|
|
45
|
+
"test_area": "test area",
|
|
46
|
+
"ref_area": "ref area",
|
|
47
|
+
"alpha_ref": r"embodied-to-operational weight $\alpha$",
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass
|
|
52
|
+
class FocalComparison:
|
|
53
|
+
"""FOCAL comparison of a ``test`` architecture against a ``ref`` baseline.
|
|
54
|
+
|
|
55
|
+
Computed under the fixed-time scenario, i.e. power is the operational
|
|
56
|
+
proxy. (Pass energy in place of power to evaluate the fixed-work scenario;
|
|
57
|
+
the NCF expression is identical.)
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
ncf: float # normalized carbon footprint NCF(test, ref)
|
|
61
|
+
embodied_ratio: float # A_test / A_ref (normalized embodied footprint)
|
|
62
|
+
operational_ratio: float # P_test / P_ref (normalized operational footprint)
|
|
63
|
+
alpha_ref: float # embodied-to-operational weight used
|
|
64
|
+
# Operating point echoed back for traceability.
|
|
65
|
+
test_pwr: float
|
|
66
|
+
ref_pwr: float
|
|
67
|
+
test_area: float
|
|
68
|
+
ref_area: float
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def more_sustainable(self) -> bool:
|
|
72
|
+
"""True if the test design has a strictly lower carbon footprint."""
|
|
73
|
+
return self.ncf < 1.0
|
|
74
|
+
|
|
75
|
+
@property
|
|
76
|
+
def verdict(self) -> str:
|
|
77
|
+
"""Human-readable verdict for the test design relative to ref."""
|
|
78
|
+
if self.ncf < 1.0:
|
|
79
|
+
return "more sustainable"
|
|
80
|
+
if self.ncf > 1.0:
|
|
81
|
+
return "less sustainable"
|
|
82
|
+
return "equally sustainable"
|
|
83
|
+
|
|
84
|
+
@property
|
|
85
|
+
def pct_change(self) -> float:
|
|
86
|
+
"""Percent change in carbon footprint of test vs ref.
|
|
87
|
+
|
|
88
|
+
Negative means a reduction (more sustainable); e.g. -30.0 means the
|
|
89
|
+
test design emits 30% less carbon than the reference.
|
|
90
|
+
"""
|
|
91
|
+
return (self.ncf - 1.0) * 100.0
|
|
92
|
+
|
|
93
|
+
@property
|
|
94
|
+
def breakeven_alpha(self) -> float | None:
|
|
95
|
+
"""Embodied weight ``alpha`` at which test and ref are carbon-equal.
|
|
96
|
+
|
|
97
|
+
Solves ``NCF(alpha) = 1`` for alpha at the current area and power
|
|
98
|
+
ratios::
|
|
99
|
+
|
|
100
|
+
alpha* = (1 - P_test/P_ref) / (A_test/A_ref - P_test/P_ref)
|
|
101
|
+
|
|
102
|
+
Returns None when the embodied and operational ratios are equal (NCF
|
|
103
|
+
does not depend on alpha, so there is no crossing). A value outside
|
|
104
|
+
[0, 1] means the verdict never flips within the valid weight range,
|
|
105
|
+
i.e. the test design is uniformly more or less sustainable.
|
|
106
|
+
"""
|
|
107
|
+
if self.embodied_ratio == self.operational_ratio:
|
|
108
|
+
return None
|
|
109
|
+
return (1.0 - self.operational_ratio) / (self.embodied_ratio - self.operational_ratio)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def focal_compare(
|
|
113
|
+
test_pwr: float,
|
|
114
|
+
ref_pwr: float,
|
|
115
|
+
test_area: float,
|
|
116
|
+
ref_area: float,
|
|
117
|
+
alpha_ref: float,
|
|
118
|
+
) -> FocalComparison:
|
|
119
|
+
"""Compare a ``test`` architecture against a ``ref`` baseline with FOCAL.
|
|
120
|
+
|
|
121
|
+
Computes the Normalized Carbon Footprint (NCF) of the test design relative
|
|
122
|
+
to the reference under the fixed-time scenario::
|
|
123
|
+
|
|
124
|
+
NCF = alpha_ref * (test_area / ref_area)
|
|
125
|
+
+ (1 - alpha_ref) * (test_pwr / ref_pwr)
|
|
126
|
+
|
|
127
|
+
``NCF < 1`` means the test design has a lower total carbon footprint than
|
|
128
|
+
the reference (more sustainable); ``NCF > 1`` means it is worse.
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
test_pwr: Power of the test design (operational proxy, numerator).
|
|
132
|
+
Substitute energy to evaluate the fixed-work scenario instead.
|
|
133
|
+
ref_pwr: Power of the reference design (operational proxy, denominator).
|
|
134
|
+
test_area: Chip area of the test design (embodied proxy, numerator).
|
|
135
|
+
ref_area: Chip area of the reference design (embodied proxy, denominator).
|
|
136
|
+
alpha_ref: Embodied-to-operational weight in [0, 1] (the FOCAL
|
|
137
|
+
``alpha_E2O``). 0.8 = embodied-dominated, 0.2 = operational-dominated.
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
A FocalComparison with the NCF, its embodied/operational components,
|
|
141
|
+
and convenience verdicts.
|
|
142
|
+
|
|
143
|
+
Raises:
|
|
144
|
+
ValueError: if a denominator is non-positive, an input is negative, or
|
|
145
|
+
``alpha_ref`` is outside [0, 1].
|
|
146
|
+
"""
|
|
147
|
+
if ref_pwr <= 0.0 or ref_area <= 0.0:
|
|
148
|
+
raise ValueError("ref_pwr and ref_area must be positive (they are denominators)")
|
|
149
|
+
if test_pwr < 0.0 or test_area < 0.0:
|
|
150
|
+
raise ValueError("test_pwr and test_area must be non-negative")
|
|
151
|
+
if not 0.0 <= alpha_ref <= 1.0:
|
|
152
|
+
raise ValueError(f"alpha_ref must be in [0, 1], got {alpha_ref}")
|
|
153
|
+
|
|
154
|
+
embodied_ratio = test_area / ref_area
|
|
155
|
+
operational_ratio = test_pwr / ref_pwr
|
|
156
|
+
ncf = alpha_ref * embodied_ratio + (1.0 - alpha_ref) * operational_ratio
|
|
157
|
+
return FocalComparison(
|
|
158
|
+
ncf=ncf,
|
|
159
|
+
embodied_ratio=embodied_ratio,
|
|
160
|
+
operational_ratio=operational_ratio,
|
|
161
|
+
alpha_ref=alpha_ref,
|
|
162
|
+
test_pwr=test_pwr,
|
|
163
|
+
ref_pwr=ref_pwr,
|
|
164
|
+
test_area=test_area,
|
|
165
|
+
ref_area=ref_area,
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
@dataclass
|
|
170
|
+
class FocalSweep:
|
|
171
|
+
"""NCF of a test/ref pair as one FOCAL parameter is swept over a range."""
|
|
172
|
+
|
|
173
|
+
parameter: str # which of the five params was swept
|
|
174
|
+
values: list[float] # swept parameter values
|
|
175
|
+
ncf: list[float] # NCF at each swept value
|
|
176
|
+
comparisons: list[FocalComparison] # full comparison at each swept value
|
|
177
|
+
breakevens: list[float] # swept values where NCF crosses 1.0
|
|
178
|
+
# Nominal operating point (the swept field holds its nominal value here).
|
|
179
|
+
test_pwr: float
|
|
180
|
+
ref_pwr: float
|
|
181
|
+
test_area: float
|
|
182
|
+
ref_area: float
|
|
183
|
+
alpha_ref: float
|
|
184
|
+
|
|
185
|
+
@property
|
|
186
|
+
def crosses_unity(self) -> bool:
|
|
187
|
+
"""True if the verdict flips (NCF crosses 1) within the swept range."""
|
|
188
|
+
return bool(self.breakevens)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
@dataclass
|
|
192
|
+
class FocalBothRefs:
|
|
193
|
+
"""Two alpha sweeps comparing arch A and arch B, each taken as the reference.
|
|
194
|
+
|
|
195
|
+
Holds the ``arch_a_ref`` sweep (A is the reference, so the test design is B)
|
|
196
|
+
and the ``arch_b_ref`` sweep (B is the reference, so the test design is A).
|
|
197
|
+
See ``sweep_both_arches_as_ref`` for why both directions matter.
|
|
198
|
+
"""
|
|
199
|
+
|
|
200
|
+
arch_a_ref: FocalSweep # arch A as reference, test design is arch B
|
|
201
|
+
arch_b_ref: FocalSweep # arch B as reference, test design is arch A
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def focal_sweep(
|
|
205
|
+
test_pwr: float,
|
|
206
|
+
ref_pwr: float,
|
|
207
|
+
test_area: float,
|
|
208
|
+
ref_area: float,
|
|
209
|
+
alpha_ref: float,
|
|
210
|
+
*,
|
|
211
|
+
sweep: str = "alpha_ref",
|
|
212
|
+
values: Sequence[float] | None = None,
|
|
213
|
+
num: int = 51,
|
|
214
|
+
span: float = 2.0,
|
|
215
|
+
plot_path: str | Path | None = None,
|
|
216
|
+
) -> FocalSweep:
|
|
217
|
+
"""Sweep one FOCAL parameter and report NCF across the range.
|
|
218
|
+
|
|
219
|
+
Holds four of the five FOCAL parameters fixed at the values passed in and
|
|
220
|
+
varies the fifth (``sweep``), recomputing the test-vs-ref NCF at each point.
|
|
221
|
+
This mirrors the paper's sensitivity analyses: sweeping ``alpha_ref`` shows
|
|
222
|
+
how the verdict shifts between operational- and embodied-dominated regimes,
|
|
223
|
+
while sweeping an area or power term traces NCF against that dimension.
|
|
224
|
+
|
|
225
|
+
Args:
|
|
226
|
+
test_pwr: Nominal test power.
|
|
227
|
+
ref_pwr: Nominal reference power.
|
|
228
|
+
test_area: Nominal test area.
|
|
229
|
+
ref_area: Nominal reference area.
|
|
230
|
+
alpha_ref: Nominal embodied-to-operational weight in [0, 1].
|
|
231
|
+
sweep: Name of the parameter to vary; one of ``SWEEPABLE_PARAMETERS``.
|
|
232
|
+
values: Explicit values to sweep. If None, a default range is built:
|
|
233
|
+
``alpha_ref`` sweeps [0, 1]; any other parameter sweeps from
|
|
234
|
+
``nominal / span`` to ``nominal * span``.
|
|
235
|
+
num: Number of points in the default range (ignored if ``values`` given).
|
|
236
|
+
span: Multiplicative half-range for non-alpha default sweeps.
|
|
237
|
+
plot_path: If given, save a PNG of NCF vs the swept parameter there
|
|
238
|
+
(requires matplotlib, imported lazily).
|
|
239
|
+
|
|
240
|
+
Returns:
|
|
241
|
+
A FocalSweep with the swept values, the NCF at each, the per-point
|
|
242
|
+
FocalComparison objects, and any NCF==1 break-even crossings.
|
|
243
|
+
|
|
244
|
+
Raises:
|
|
245
|
+
ValueError: if ``sweep`` is not a FOCAL parameter, or a default range
|
|
246
|
+
cannot be built because the nominal swept value is non-positive.
|
|
247
|
+
"""
|
|
248
|
+
if sweep not in SWEEPABLE_PARAMETERS:
|
|
249
|
+
raise ValueError(f"sweep must be one of {SWEEPABLE_PARAMETERS}, got {sweep!r}")
|
|
250
|
+
if sweep in ("ref_pwr", "ref_area"):
|
|
251
|
+
_warn_ref_sweep(sweep)
|
|
252
|
+
|
|
253
|
+
base = {
|
|
254
|
+
"test_pwr": test_pwr,
|
|
255
|
+
"ref_pwr": ref_pwr,
|
|
256
|
+
"test_area": test_area,
|
|
257
|
+
"ref_area": ref_area,
|
|
258
|
+
"alpha_ref": alpha_ref,
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if values is not None:
|
|
262
|
+
swept_values = [float(v) for v in values]
|
|
263
|
+
elif sweep == "alpha_ref":
|
|
264
|
+
swept_values = _linspace(0.0, 1.0, num)
|
|
265
|
+
else:
|
|
266
|
+
nominal = base[sweep]
|
|
267
|
+
if nominal <= 0.0:
|
|
268
|
+
raise ValueError(
|
|
269
|
+
f"cannot auto-range {sweep} from a non-positive nominal "
|
|
270
|
+
f"({nominal}); pass explicit values=..."
|
|
271
|
+
)
|
|
272
|
+
swept_values = _linspace(nominal / span, nominal * span, num)
|
|
273
|
+
|
|
274
|
+
comparisons: list[FocalComparison] = []
|
|
275
|
+
for v in swept_values:
|
|
276
|
+
params = dict(base)
|
|
277
|
+
params[sweep] = v
|
|
278
|
+
comparisons.append(focal_compare(**params))
|
|
279
|
+
|
|
280
|
+
ncf = [c.ncf for c in comparisons]
|
|
281
|
+
breakevens = _level_crossings(swept_values, ncf, 1.0)
|
|
282
|
+
|
|
283
|
+
result = FocalSweep(
|
|
284
|
+
parameter=sweep,
|
|
285
|
+
values=swept_values,
|
|
286
|
+
ncf=ncf,
|
|
287
|
+
comparisons=comparisons,
|
|
288
|
+
breakevens=breakevens,
|
|
289
|
+
test_pwr=test_pwr,
|
|
290
|
+
ref_pwr=ref_pwr,
|
|
291
|
+
test_area=test_area,
|
|
292
|
+
ref_area=ref_area,
|
|
293
|
+
alpha_ref=alpha_ref,
|
|
294
|
+
)
|
|
295
|
+
if plot_path is not None:
|
|
296
|
+
_plot_sweep(result, plot_path)
|
|
297
|
+
return result
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def sweep_both_arches_as_ref(
|
|
301
|
+
arch_a_pwr: float,
|
|
302
|
+
arch_a_area: float,
|
|
303
|
+
arch_b_pwr: float,
|
|
304
|
+
arch_b_area: float,
|
|
305
|
+
*,
|
|
306
|
+
values: Sequence[float] | None = None,
|
|
307
|
+
num: int = 51,
|
|
308
|
+
plot_path: str | Path | None = None,
|
|
309
|
+
) -> FocalBothRefs:
|
|
310
|
+
"""Sweep alpha comparing two architectures with *each* used as the reference.
|
|
311
|
+
|
|
312
|
+
The NCF metric is not symmetric: ``NCF(X, Y) != 1 / NCF(Y, X)`` in general,
|
|
313
|
+
because NCF is a weighted *sum* of the area and power ratios rather than a
|
|
314
|
+
single ratio (in fact ``NCF(X, Y) * NCF(Y, X) >= 1``). The reference design
|
|
315
|
+
is the denominator, so for the *same* alpha you can get a different NCF — and
|
|
316
|
+
even a different verdict — depending on which architecture you pick as the
|
|
317
|
+
reference. There is no privileged "baseline" here, so it is worth looking at
|
|
318
|
+
both directions before concluding that one design is more sustainable.
|
|
319
|
+
|
|
320
|
+
This runs two alpha sweeps over the same grid: one with arch A as the
|
|
321
|
+
reference (test design is B) and one with arch B as the reference (test
|
|
322
|
+
design is A). Compare the two curves at a given alpha to see the asymmetry.
|
|
323
|
+
|
|
324
|
+
Args:
|
|
325
|
+
arch_a_pwr: Power of architecture A.
|
|
326
|
+
arch_a_area: Chip area of architecture A.
|
|
327
|
+
arch_b_pwr: Power of architecture B.
|
|
328
|
+
arch_b_area: Chip area of architecture B.
|
|
329
|
+
values: Explicit alpha values to sweep. If None, sweeps [0, 1].
|
|
330
|
+
num: Number of alpha points in the default range (ignored if ``values``).
|
|
331
|
+
plot_path: If given, save a PNG overlaying both NCF curves vs alpha
|
|
332
|
+
(requires matplotlib, imported lazily).
|
|
333
|
+
|
|
334
|
+
Returns:
|
|
335
|
+
A FocalBothRefs holding the ``arch_a_ref`` and ``arch_b_ref`` sweeps.
|
|
336
|
+
"""
|
|
337
|
+
# Placeholder alpha_ref; it is overridden at every point of the alpha sweep.
|
|
338
|
+
arch_a_ref = focal_sweep(
|
|
339
|
+
test_pwr=arch_b_pwr, ref_pwr=arch_a_pwr,
|
|
340
|
+
test_area=arch_b_area, ref_area=arch_a_area,
|
|
341
|
+
alpha_ref=0.5, sweep="alpha_ref", values=values, num=num,
|
|
342
|
+
)
|
|
343
|
+
arch_b_ref = focal_sweep(
|
|
344
|
+
test_pwr=arch_a_pwr, ref_pwr=arch_b_pwr,
|
|
345
|
+
test_area=arch_a_area, ref_area=arch_b_area,
|
|
346
|
+
alpha_ref=0.5, sweep="alpha_ref", values=values, num=num,
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
result = FocalBothRefs(arch_a_ref=arch_a_ref, arch_b_ref=arch_b_ref)
|
|
350
|
+
if plot_path is not None:
|
|
351
|
+
_plot_both_refs(result, plot_path)
|
|
352
|
+
return result
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def _warn_ref_sweep(sweep: str) -> None:
|
|
356
|
+
"""Loudly warn that sweeping a reference value at constant alpha_ref is dicey.
|
|
357
|
+
|
|
358
|
+
Fires when ``focal_sweep`` is asked to vary ``ref_pwr`` or ``ref_area``:
|
|
359
|
+
holding ``alpha_ref`` fixed across such a sweep is internally inconsistent,
|
|
360
|
+
because the embodied-to-operational weight that actually applies to the
|
|
361
|
+
reference design depends on its own power and area.
|
|
362
|
+
"""
|
|
363
|
+
message = (
|
|
364
|
+
"sweeping reference values with a constant alpha_ref yields difficult "
|
|
365
|
+
"to understand results, since the true value of alpha_ref is a function "
|
|
366
|
+
"of the ref_pwr and ref_area"
|
|
367
|
+
)
|
|
368
|
+
width = 76
|
|
369
|
+
bar = "!" * width
|
|
370
|
+
lines = ["", bar, "!!" + " FOCAL WARNING ".center(width - 4) + "!!", bar]
|
|
371
|
+
for line in textwrap.wrap(f"You are sweeping {sweep!r}: {message}.", width - 6):
|
|
372
|
+
lines.append("!! " + line.ljust(width - 6) + " !!")
|
|
373
|
+
lines += [bar, ""]
|
|
374
|
+
print("\n".join(lines), file=sys.stderr)
|
|
375
|
+
warnings.warn(message, stacklevel=3)
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def _linspace(lo: float, hi: float, num: int) -> list[float]:
|
|
379
|
+
"""Evenly spaced values from lo to hi inclusive (pure-Python linspace)."""
|
|
380
|
+
if num < 1:
|
|
381
|
+
return []
|
|
382
|
+
if num == 1:
|
|
383
|
+
return [lo]
|
|
384
|
+
step = (hi - lo) / (num - 1)
|
|
385
|
+
return [lo + step * i for i in range(num)]
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def _level_crossings(xs: list[float], ys: list[float], level: float = 1.0) -> list[float]:
|
|
389
|
+
"""Find x where the piecewise-linear (xs, ys) curve crosses ``level``.
|
|
390
|
+
|
|
391
|
+
Returns the x of every exact hit and every sign-change crossing (located by
|
|
392
|
+
linear interpolation between the bracketing samples).
|
|
393
|
+
"""
|
|
394
|
+
crossings: list[float] = []
|
|
395
|
+
for i in range(len(xs) - 1):
|
|
396
|
+
a = ys[i] - level
|
|
397
|
+
b = ys[i + 1] - level
|
|
398
|
+
if a == 0.0:
|
|
399
|
+
crossings.append(xs[i])
|
|
400
|
+
elif (a < 0.0 < b) or (b < 0.0 < a):
|
|
401
|
+
t = a / (a - b)
|
|
402
|
+
crossings.append(xs[i] + t * (xs[i + 1] - xs[i]))
|
|
403
|
+
if ys and ys[-1] - level == 0.0:
|
|
404
|
+
crossings.append(xs[-1])
|
|
405
|
+
return crossings
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
def _plot_sweep(result: FocalSweep, plot_path: str | Path) -> None:
|
|
409
|
+
"""Save a PNG of NCF vs the swept parameter (matplotlib imported lazily)."""
|
|
410
|
+
import matplotlib
|
|
411
|
+
|
|
412
|
+
matplotlib.use("Agg")
|
|
413
|
+
import matplotlib.pyplot as plt
|
|
414
|
+
|
|
415
|
+
fig, ax = plt.subplots(figsize=(7, 5))
|
|
416
|
+
ax.plot(result.values, result.ncf, color="#2b6cb0", marker="o",
|
|
417
|
+
markersize=3, linewidth=1.5, label="NCF(test, ref)")
|
|
418
|
+
ax.axhline(1.0, color="grey", linestyle="--", linewidth=1.0,
|
|
419
|
+
label="carbon-equal (NCF = 1)")
|
|
420
|
+
|
|
421
|
+
ymin, ymax = ax.get_ylim()
|
|
422
|
+
ax.axhspan(ymin, 1.0, color="#c6f6d5", alpha=0.3, zorder=0,
|
|
423
|
+
label="test more sustainable")
|
|
424
|
+
ax.set_ylim(ymin, ymax)
|
|
425
|
+
|
|
426
|
+
for i, be in enumerate(result.breakevens):
|
|
427
|
+
ax.axvline(be, color="#e53e3e", linestyle=":", linewidth=1.0,
|
|
428
|
+
label="break-even" if i == 0 else None)
|
|
429
|
+
|
|
430
|
+
ax.set_xlabel(_PARAM_LABELS.get(result.parameter, result.parameter))
|
|
431
|
+
ax.set_ylabel("normalized carbon footprint (NCF)")
|
|
432
|
+
ax.set_title(f"FOCAL sweep over {result.parameter}")
|
|
433
|
+
ax.grid(alpha=0.3)
|
|
434
|
+
ax.legend(loc="best", fontsize=8)
|
|
435
|
+
fig.tight_layout()
|
|
436
|
+
|
|
437
|
+
out = Path(plot_path)
|
|
438
|
+
out.parent.mkdir(parents=True, exist_ok=True)
|
|
439
|
+
fig.savefig(out, dpi=150)
|
|
440
|
+
plt.close(fig)
|
|
441
|
+
print(f" FOCAL sweep plot -> {out}")
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
def _plot_both_refs(result: FocalBothRefs, plot_path: str | Path) -> None:
|
|
445
|
+
"""Overlay the two reference-choice NCF curves vs alpha (lazy matplotlib)."""
|
|
446
|
+
import matplotlib
|
|
447
|
+
|
|
448
|
+
matplotlib.use("Agg")
|
|
449
|
+
import matplotlib.pyplot as plt
|
|
450
|
+
|
|
451
|
+
fig, ax = plt.subplots(figsize=(7, 5))
|
|
452
|
+
ax.plot(result.arch_a_ref.values, result.arch_a_ref.ncf, color="#2b6cb0",
|
|
453
|
+
marker="o", markersize=3, linewidth=1.5,
|
|
454
|
+
label="A as ref (NCF of B vs A)")
|
|
455
|
+
ax.plot(result.arch_b_ref.values, result.arch_b_ref.ncf, color="#dd6b20",
|
|
456
|
+
marker="s", markersize=3, linewidth=1.5,
|
|
457
|
+
label="B as ref (NCF of A vs B)")
|
|
458
|
+
ax.axhline(1.0, color="grey", linestyle="--", linewidth=1.0,
|
|
459
|
+
label="carbon-equal (NCF = 1)")
|
|
460
|
+
|
|
461
|
+
ax.set_xlabel(_PARAM_LABELS["alpha_ref"])
|
|
462
|
+
ax.set_ylabel("normalized carbon footprint (NCF)")
|
|
463
|
+
ax.set_title("FOCAL: same alpha, each architecture as reference")
|
|
464
|
+
ax.grid(alpha=0.3)
|
|
465
|
+
ax.legend(loc="best", fontsize=8)
|
|
466
|
+
fig.tight_layout()
|
|
467
|
+
|
|
468
|
+
out = Path(plot_path)
|
|
469
|
+
out.parent.mkdir(parents=True, exist_ok=True)
|
|
470
|
+
fig.savefig(out, dpi=150)
|
|
471
|
+
plt.close(fig)
|
|
472
|
+
print(f" FOCAL both-refs plot -> {out}")
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""PFAS-in-semiconductor-manufacturing mask data for chip PFAS/carbon modeling.
|
|
2
|
+
|
|
3
|
+
All data in this module is derived from the following work, and full credit
|
|
4
|
+
for the underlying analysis belongs to its authors:
|
|
5
|
+
|
|
6
|
+
Mariam Elgamal, Abdulrahman Mahmoud, Gu-Yeon Wei, David Brooks, and
|
|
7
|
+
Gage Hills. "Modeling PFAS in Semiconductor Manufacturing to Quantify
|
|
8
|
+
Trade-offs in Energy Efficiency and Environmental Impact of Computing
|
|
9
|
+
Systems." arXiv preprint arXiv:2505.06727 (2025).
|
|
10
|
+
|
|
11
|
+
Abstract: https://arxiv.org/abs/2505.06727
|
|
12
|
+
PDF: https://arxiv.org/pdf/2505.06727
|
|
13
|
+
|
|
14
|
+
Any use of this node should cite this work.
|
|
15
|
+
|
|
16
|
+
The paper uses the number of lithography masks as a first-order proxy for the
|
|
17
|
+
amount of PFAS ("forever chemicals") used in chip manufacturing, because the
|
|
18
|
+
PFAS-containing layers (photoresist, anti-reflective coatings, topcoats) scale
|
|
19
|
+
with mask count -- eq. (1): #PFAS_litho ~ number of lithography masks.
|
|
20
|
+
|
|
21
|
+
Data provenance within the paper:
|
|
22
|
+
* BEOL_METAL_STACK_TYPES -- lithography masks per BEOL metal-stack recipe.
|
|
23
|
+
Mask counts follow Table II ("Number of lithography process steps and masks
|
|
24
|
+
per metal line process"); the recipe names are the BEOL stack types in the
|
|
25
|
+
Figure 5 legend, read top-to-bottom.
|
|
26
|
+
* BEOL_PROCESSES_PER_NODE -- per-node BEOL metal-stack composition, decoded
|
|
27
|
+
from the stacked bars of Figure 5.
|
|
28
|
+
* PROCESSES_PER_NODE -- the full per-node stack: FEOL and MOL layers (counts
|
|
29
|
+
from Figure 5) prepended to the BEOL layers above.
|
|
30
|
+
|
|
31
|
+
Any transcription errors here are ours, not the authors'.
|
|
32
|
+
|
|
33
|
+
The PFAS proxy (lithography masks * area/yield) is only meaningful when considered relative
|
|
34
|
+
to other instances.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
# BEOL metal-stack type -> total lithography masks (metal-layer process + via-
|
|
38
|
+
# layer process), read top-to-bottom from the Figure 5 legend (arXiv:2505.06727).
|
|
39
|
+
# Keys match the recipe strings used in BEOL_PROCESSES_PER_NODE, so a node's BEOL
|
|
40
|
+
# mask count is sum(BEOL_METAL_STACK_TYPES[r] for r in BEOL_PROCESSES_PER_NODE[node]).
|
|
41
|
+
BEOL_METAL_STACK_TYPES: dict[str, int] = {
|
|
42
|
+
"M: ArF, V: ArF": 2,
|
|
43
|
+
"M: ArFi, V: ArFi": 2,
|
|
44
|
+
"M: ArFi LE, V: ArFi LE": 3,
|
|
45
|
+
"M: ArFi SADP + ArFi LE block, V: ArFi LE-2": 4,
|
|
46
|
+
"M: ArFi SADP + ArFi SAB, V: ArFi LE-4": 7,
|
|
47
|
+
"M: ArFi SAQP + ArFi SAB, V: ArFi LE-4": 7,
|
|
48
|
+
"M: EUV LE, V: EUV LE": 2,
|
|
49
|
+
"M: ArFi SAQP + EUV LE block, V: EUV LE": 3,
|
|
50
|
+
"M: ArFi SAQP + EUV LE block, V: EUV LE-2": 4,
|
|
51
|
+
"M: ArFi SAQP + EUV LE-2, V: EUV LE-2": 5,
|
|
52
|
+
"M: EUV LE-2 + EUV SAB, V: EUV LE-2": 6,
|
|
53
|
+
"M: i-line, V: ArF": 2,
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
# Per-node BEOL metal stack from Figure 5 of arXiv:2505.06727
|
|
57
|
+
# Order is bottom (finest) -> top (coarsest).
|
|
58
|
+
BEOL_PROCESSES_PER_NODE: dict[str, list[str]] = {
|
|
59
|
+
"130nm": ["M: ArF, V: ArF"]*5 + ["M: i-line, V: ArF"],
|
|
60
|
+
"90nm": ["M: ArF, V: ArF"]*5 + ["M: i-line, V: ArF"],
|
|
61
|
+
"65nm": ["M: ArF, V: ArF"]*8 + ["M: i-line, V: ArF"],
|
|
62
|
+
"40nm": ["M: ArFi, V: ArFi"]*5 + ["M: ArF, V: ArF"]*4 + ["M: i-line, V: ArF"],
|
|
63
|
+
"28nm": ["M: ArFi, V: ArFi"]*7 + ["M: ArF, V: ArF"]*2 + ["M: i-line, V: ArF"],
|
|
64
|
+
"20nm": ["M: ArFi LE, V: ArFi LE"]*3 + ["M: ArFi, V: ArFi"]*4 + ["M: ArF, V: ArF"]*2 + ["M: i-line, V: ArF"],
|
|
65
|
+
"16nm": ["M: ArFi LE, V: ArFi LE"]*3 + ["M: ArFi, V: ArFi"]*4 + ["M: ArF, V: ArF"]*2 + ["M: i-line, V: ArF"],
|
|
66
|
+
"10nm": ["M: ArFi SADP + ArFi LE block, V: ArFi LE-2"]*3 + ["M: ArFi LE, V: ArFi LE"]*4 + ["M: ArFi, V: ArFi"]*3 + ["M: i-line, V: ArF"],
|
|
67
|
+
"7nm": ["M: ArFi SADP + ArFi SAB, V: ArFi LE-4"]*1 + ["M: ArFi SAQP + ArFi SAB, V: ArFi LE-4"]*2 + ["M: ArFi SADP + ArFi LE block, V: ArFi LE-2"]*4 + ["M: ArFi, V: ArFi"]*7 + ["M: i-line, V: ArF"],
|
|
68
|
+
"7nm+": ["M: EUV LE, V: EUV LE"]*3 + ["M: ArFi SADP + ArFi LE block, V: ArFi LE-2"]*4 + ["M: ArFi, V: ArFi"]*7 + ["M: i-line, V: ArF"],
|
|
69
|
+
"5nm": ["M: ArFi SAQP + EUV LE-2, V: EUV LE-2"]*1 + ["M: ArFi SAQP + EUV LE block, V: EUV LE-2"]*2 + ["M: ArFi SAQP + EUV LE block, V: EUV LE"]*2 + ["M: ArFi SADP + ArFi LE block, V: ArFi LE-2"]*2 + ["M: ArFi, V: ArFi"]*9 + ["M: i-line, V: ArF"],
|
|
70
|
+
"3nm": ["M: EUV LE-2 + EUV SAB, V: EUV LE-2"]*2 + ["M: ArFi SAQP + EUV LE-2, V: EUV LE-2"]*2 + ["M: ArFi SAQP + EUV LE block, V: EUV LE"]*2 + ["M: ArFi SADP + ArFi LE block, V: ArFi LE-2"]*2 + ["M: ArFi, V: ArFi"]*11 + ["M: i-line, V: ArF"],
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
# FEOL and MOL lithography-layer counts per node (each is one PFAS mask layer),
|
|
74
|
+
# from arXiv:2505.06727 (the FEOL/MOL portions of the Figure 5 stacked bars).
|
|
75
|
+
_FEOL_LAYERS_PER_NODE: dict[str, int] = {
|
|
76
|
+
n: (8 if n == "130nm" else 10) for n in BEOL_PROCESSES_PER_NODE
|
|
77
|
+
}
|
|
78
|
+
_MOL_LAYERS_PER_NODE: dict[str, int] = {
|
|
79
|
+
n: (1 if n in ("130nm", "90nm", "65nm", "40nm", "28nm", "20nm") else 2)
|
|
80
|
+
for n in BEOL_PROCESSES_PER_NODE
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
# Full per-node lithography stack, bottom -> top: FEOL, then MOL, then the BEOL
|
|
84
|
+
# metal layers. Each "FEOL"/"MOL" entry is one mask layer; the BEOL entries are
|
|
85
|
+
# metal-stack recipes priced by BEOL_METAL_STACK_TYPES. A node's total PFAS mask
|
|
86
|
+
# layers is (# "FEOL") + (# "MOL") + sum(BEOL_METAL_STACK_TYPES[r] for the BEOL
|
|
87
|
+
# entries r).
|
|
88
|
+
PROCESSES_PER_NODE: dict[str, list[str]] = {
|
|
89
|
+
n: ["FEOL"] * _FEOL_LAYERS_PER_NODE[n]
|
|
90
|
+
+ ["MOL"] * _MOL_LAYERS_PER_NODE[n]
|
|
91
|
+
+ beol
|
|
92
|
+
for n, beol in BEOL_PROCESSES_PER_NODE.items()
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def num_lith_masks(node: str) -> int:
|
|
97
|
+
"""Number of PFAS-containing lithography masks for ``node``.
|
|
98
|
+
|
|
99
|
+
Sums the full per-node stack in PROCESSES_PER_NODE: each FEOL and MOL layer
|
|
100
|
+
counts as one mask, and each BEOL metal-stack recipe is priced by
|
|
101
|
+
BEOL_METAL_STACK_TYPES (paper eq. 1: #PFAS_litho ~ number of masks).
|
|
102
|
+
"""
|
|
103
|
+
if node not in PROCESSES_PER_NODE:
|
|
104
|
+
raise KeyError(f"unknown node {node!r}; known nodes: {list(PROCESSES_PER_NODE)}")
|
|
105
|
+
return sum(
|
|
106
|
+
1 if layer in ("FEOL", "MOL") else BEOL_METAL_STACK_TYPES[layer]
|
|
107
|
+
for layer in PROCESSES_PER_NODE[node]
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def get_PFAS_proxy(node: str, area: float, yield_: float) -> float:
|
|
112
|
+
"""PFAS manufacturing proxy for ``node`` (paper eq. 2)::
|
|
113
|
+
|
|
114
|
+
PFAS_chip_manufacturing = #PFAS_litho * Area / Yield
|
|
115
|
+
|
|
116
|
+
where #PFAS_litho is ``num_lith_masks(node)``. ``area`` is die area (in
|
|
117
|
+
whatever unit you use consistently) and ``yield_`` is the manufacturing
|
|
118
|
+
yield in (0, 1]. (``yield`` is a Python keyword, hence the trailing
|
|
119
|
+
underscore.)
|
|
120
|
+
|
|
121
|
+
Like the mask proxy itself, the result is only meaningful
|
|
122
|
+
relative to other instances computed the same way. This is
|
|
123
|
+
not an absolute quantity of PFAS.
|
|
124
|
+
"""
|
|
125
|
+
if not 0.0 < yield_ <= 1.0:
|
|
126
|
+
raise ValueError(f"yield_ must be in (0, 1], got {yield_}")
|
|
127
|
+
return num_lith_masks(node) * area / yield_
|
chia/aws/__init__.py
ADDED
|
File without changes
|