diff --git a/src/any_circuit_element.ts b/src/any_circuit_element.ts index 10fb9381..500ec53a 100644 --- a/src/any_circuit_element.ts +++ b/src/any_circuit_element.ts @@ -151,6 +151,7 @@ export const any_circuit_element = z.union([ sim.simulation_switch, sim.simulation_voltage_probe, sim.simulation_current_probe, + sim.simulation_oscilloscope_trace, sim.simulation_unknown_experiment_error, sim.simulation_op_amp, sim.simulation_spice_subcircuit, diff --git a/src/simulation/index.ts b/src/simulation/index.ts index 18851101..c2cce3fc 100644 --- a/src/simulation/index.ts +++ b/src/simulation/index.ts @@ -9,3 +9,4 @@ export * from "./simulation_current_probe" export * from "./simulation_unknown_experiment_error" export * from "./simulation_op_amp" export * from "./simulation_spice_subcircuit" +export * from "./simulation_oscilloscope_trace" diff --git a/src/simulation/simulation_current_probe.ts b/src/simulation/simulation_current_probe.ts index eee41882..601f9869 100644 --- a/src/simulation/simulation_current_probe.ts +++ b/src/simulation/simulation_current_probe.ts @@ -2,13 +2,6 @@ import { z } from "zod" import { getZodPrefixedIdWithDefault } from "src/common" import { expectTypesMatch } from "src/utils/expect-types-match" -export const simulation_current_probe_display_options = z.object({ - label: z.string().optional(), - center: z.number().optional(), - offset_divs: z.number().optional(), - units_per_div: z.number().optional(), -}) - export const simulation_current_probe = z .object({ type: z.literal("simulation_current_probe"), @@ -23,7 +16,6 @@ export const simulation_current_probe = z negative_source_net_id: z.string().optional(), subcircuit_id: z.string().optional(), color: z.string().optional(), - display_options: simulation_current_probe_display_options.optional(), }) .describe( "Defines a current probe for simulation. It measures current flowing from the positive endpoint to the negative endpoint.", @@ -77,9 +69,6 @@ export type SimulationCurrentProbeInput = z.input< typeof simulation_current_probe > type InferredSimulationCurrentProbe = z.infer -type InferredSimulationCurrentProbeDisplayOptions = z.infer< - typeof simulation_current_probe_display_options -> /** * Defines a current probe for simulation. It measures current flowing from the @@ -96,18 +85,6 @@ export interface SimulationCurrentProbe { negative_source_net_id?: string subcircuit_id?: string color?: string - display_options?: SimulationCurrentProbeDisplayOptions -} - -export interface SimulationCurrentProbeDisplayOptions { - label?: string - center?: number - offset_divs?: number - units_per_div?: number } -expectTypesMatch< - SimulationCurrentProbeDisplayOptions, - InferredSimulationCurrentProbeDisplayOptions ->(true) expectTypesMatch(true) diff --git a/src/simulation/simulation_oscilloscope_trace.ts b/src/simulation/simulation_oscilloscope_trace.ts new file mode 100644 index 00000000..47ba8b0c --- /dev/null +++ b/src/simulation/simulation_oscilloscope_trace.ts @@ -0,0 +1,93 @@ +import { z } from "zod" +import { getZodPrefixedIdWithDefault } from "src/common" +import { expectTypesMatch } from "src/utils/expect-types-match" + +const hasValue = (value: unknown) => value !== undefined + +export const simulation_oscilloscope_trace = z + .object({ + type: z.literal("simulation_oscilloscope_trace"), + simulation_oscilloscope_trace_id: getZodPrefixedIdWithDefault( + "simulation_oscilloscope_trace", + ), + simulation_transient_voltage_graph_id: z.string().optional(), + simulation_transient_current_graph_id: z.string().optional(), + simulation_voltage_probe_id: z.string().optional(), + simulation_current_probe_id: z.string().optional(), + display_name: z.string().optional(), + color: z.string().optional(), + volts_per_div: z.number().positive().optional(), + amps_per_div: z.number().positive().optional(), + vertical_center: z.number().optional(), + vertical_offset_divs: z.number().optional(), + }) + .describe( + "Defines how a simulation probe or transient graph is rendered as an oscilloscope-style trace.", + ) + .superRefine((data, ctx) => { + const voltageReferences = [ + data.simulation_transient_voltage_graph_id, + data.simulation_voltage_probe_id, + ].filter(hasValue).length + const currentReferences = [ + data.simulation_transient_current_graph_id, + data.simulation_current_probe_id, + ].filter(hasValue).length + + if (voltageReferences + currentReferences !== 1) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: + "An oscilloscope trace must reference exactly one voltage graph, current graph, voltage probe, or current probe.", + }) + } + + if (voltageReferences > 0 && data.amps_per_div !== undefined) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: + "Voltage oscilloscope traces must use volts_per_div, not amps_per_div.", + }) + } + + if (currentReferences > 0 && data.volts_per_div !== undefined) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: + "Current oscilloscope traces must use amps_per_div, not volts_per_div.", + }) + } + }) + +export type SimulationOscilloscopeTraceInput = z.input< + typeof simulation_oscilloscope_trace +> +type InferredSimulationOscilloscopeTrace = z.infer< + typeof simulation_oscilloscope_trace +> + +/** + * Defines how a simulation probe or transient graph is rendered as an + * oscilloscope-style trace. Scale and legend properties live here because they + * describe the relationship between a measurement and a graph, not the probe + * itself. + */ +export interface SimulationOscilloscopeTrace { + type: "simulation_oscilloscope_trace" + simulation_oscilloscope_trace_id: string + simulation_transient_voltage_graph_id?: string + simulation_transient_current_graph_id?: string + simulation_voltage_probe_id?: string + simulation_current_probe_id?: string + display_name?: string + color?: string + volts_per_div?: number + amps_per_div?: number + vertical_center?: number + vertical_offset_divs?: number +} + +expectTypesMatch< + SimulationOscilloscopeTrace, + InferredSimulationOscilloscopeTrace +>(true) diff --git a/src/simulation/simulation_voltage_probe.ts b/src/simulation/simulation_voltage_probe.ts index 99e8056c..6b0e7876 100644 --- a/src/simulation/simulation_voltage_probe.ts +++ b/src/simulation/simulation_voltage_probe.ts @@ -2,13 +2,6 @@ import { z } from "zod" import { getZodPrefixedIdWithDefault } from "src/common" import { expectTypesMatch } from "src/utils/expect-types-match" -export const simulation_voltage_probe_display_options = z.object({ - label: z.string().optional(), - center: z.number().optional(), - offset_divs: z.number().optional(), - units_per_div: z.number().optional(), -}) - export const simulation_voltage_probe = z .object({ type: z.literal("simulation_voltage_probe"), @@ -23,7 +16,6 @@ export const simulation_voltage_probe = z reference_input_source_net_id: z.string().optional(), subcircuit_id: z.string().optional(), color: z.string().optional(), - display_options: simulation_voltage_probe_display_options.optional(), }) .describe( "Defines a voltage probe for simulation. If a reference input is not provided, it measures against ground. If a reference input is provided, it measures the differential voltage between two points.", @@ -87,9 +79,6 @@ export type SimulationVoltageProbeInput = z.input< typeof simulation_voltage_probe > type InferredSimulationVoltageProbe = z.infer -type InferredSimulationVoltageProbeDisplayOptions = z.infer< - typeof simulation_voltage_probe_display_options -> /** * Defines a voltage probe for simulation. If a reference input is not provided, @@ -107,18 +96,6 @@ export interface SimulationVoltageProbe { reference_input_source_net_id?: string subcircuit_id?: string color?: string - display_options?: SimulationVoltageProbeDisplayOptions -} - -export interface SimulationVoltageProbeDisplayOptions { - label?: string - center?: number - offset_divs?: number - units_per_div?: number } -expectTypesMatch< - SimulationVoltageProbeDisplayOptions, - InferredSimulationVoltageProbeDisplayOptions ->(true) expectTypesMatch(true) diff --git a/tests/simulation_current_probe.test.ts b/tests/simulation_current_probe.test.ts index 94f79719..13fe81b7 100644 --- a/tests/simulation_current_probe.test.ts +++ b/tests/simulation_current_probe.test.ts @@ -61,19 +61,12 @@ test("simulation_current_probe with one port id should throw", () => { expect(() => simulation_current_probe.parse(input)).toThrow() }) -test("simulation_current_probe parses with name and display options", () => { +test("simulation_current_probe parses with name", () => { const input: SimulationCurrentProbeInput = { type: "simulation_current_probe", positive_source_net_id: "net1", negative_source_net_id: "net2", name: "My Current Probe", - color: "#ff8c00", - display_options: { - label: "IO", - center: 0.01, - offset_divs: 2, - units_per_div: 0.001, - }, } const result = simulation_current_probe.parse(input) @@ -82,13 +75,6 @@ test("simulation_current_probe parses with name and display options", () => { expect(probe.positive_source_net_id).toBe("net1") expect(probe.negative_source_net_id).toBe("net2") expect(probe.name).toBe("My Current Probe") - expect(probe.display_options).toEqual({ - label: "IO", - center: 0.01, - offset_divs: 2, - units_per_div: 0.001, - }) - expect(probe.color).toBe("#ff8c00") expect(() => any_circuit_element.parse(input)).not.toThrow() }) diff --git a/tests/simulation_experiment.test.ts b/tests/simulation_experiment.test.ts index 643b1f35..25d0e3e9 100644 --- a/tests/simulation_experiment.test.ts +++ b/tests/simulation_experiment.test.ts @@ -5,6 +5,7 @@ import { } from "../src/simulation/simulation_experiment" import { simulation_transient_voltage_graph } from "../src/simulation/simulation_transient_voltage_graph" import { simulation_transient_current_graph } from "../src/simulation/simulation_transient_current_graph" +import { simulation_oscilloscope_trace } from "../src/simulation/simulation_oscilloscope_trace" import { any_circuit_element } from "../src/any_circuit_element" test("simulation_experiment requires valid experiment_type", () => { @@ -86,3 +87,52 @@ test("simulation_transient_current_graph parses required data", () => { expect(graph.start_time_ms).toBe(0) expect(graph.end_time_ms).toBe(2) }) + +test("simulation_oscilloscope_trace configures voltage graph display", () => { + const trace = simulation_oscilloscope_trace.parse({ + type: "simulation_oscilloscope_trace", + simulation_transient_voltage_graph_id: + "simulation_transient_voltage_graph_123", + display_name: "VOUT", + color: "#315cff", + volts_per_div: 0.05, + vertical_center: 3.3, + vertical_offset_divs: 3, + }) + + expect(trace.simulation_oscilloscope_trace_id).toBeString() + expect(trace.display_name).toBe("VOUT") + expect(trace.volts_per_div).toBe(0.05) + expect(() => any_circuit_element.parse(trace)).not.toThrow() +}) + +test("simulation_oscilloscope_trace configures current probe display", () => { + const trace = simulation_oscilloscope_trace.parse({ + type: "simulation_oscilloscope_trace", + simulation_current_probe_id: "simulation_current_probe_123", + display_name: "IOUT", + amps_per_div: 0.001, + }) + + expect(trace.simulation_current_probe_id).toBe("simulation_current_probe_123") + expect(trace.amps_per_div).toBe(0.001) + expect(() => any_circuit_element.parse(trace)).not.toThrow() +}) + +test("simulation_oscilloscope_trace rejects ambiguous references and units", () => { + expect(() => + simulation_oscilloscope_trace.parse({ + type: "simulation_oscilloscope_trace", + simulation_voltage_probe_id: "simulation_voltage_probe_123", + simulation_current_probe_id: "simulation_current_probe_123", + }), + ).toThrow() + + expect(() => + simulation_oscilloscope_trace.parse({ + type: "simulation_oscilloscope_trace", + simulation_voltage_probe_id: "simulation_voltage_probe_123", + amps_per_div: 0.001, + }), + ).toThrow() +}) diff --git a/tests/simulation_voltage_probe.test.ts b/tests/simulation_voltage_probe.test.ts index c5f489e9..a3555aaa 100644 --- a/tests/simulation_voltage_probe.test.ts +++ b/tests/simulation_voltage_probe.test.ts @@ -96,33 +96,6 @@ test("simulation_voltage_probe parses with name", () => { expect(probe.name).toBe("My Probe") }) -test("simulation_voltage_probe parses with display options", () => { - const input: SimulationVoltageProbeInput = { - type: "simulation_voltage_probe", - signal_input_source_net_id: "net1", - name: "VOUT_PROBE", - color: "#315cff", - display_options: { - label: "VO", - center: 3.3, - offset_divs: 3, - units_per_div: 0.05, - }, - } - - const result = simulation_voltage_probe.parse(input) - const probe = result as SimulationVoltageProbe - - expect(probe.display_options).toEqual({ - label: "VO", - center: 3.3, - offset_divs: 3, - units_per_div: 0.05, - }) - expect(probe.color).toBe("#315cff") - expect(() => any_circuit_element.parse(input)).not.toThrow() -}) - test("simulation_voltage_probe (differential) with mixed connections should throw", () => { const input: SimulationVoltageProbeInput = { type: "simulation_voltage_probe",