Exercises

ex-sp-ch03-01

Easy

Create a @dataclass called ExperimentConfig with fields: n_samples (int, default 1000), snr_db (float, default 20.0), algorithm (str, default "lasso"), and tags (list of str, default empty). Make it frozen and verify that you cannot modify attributes after creation.

ex-sp-ch03-02

Easy

Implement a Vector3D class with x, y, z attributes and the following dunder methods: __repr__, __add__ (vector addition), __mul__ (scalar multiplication), and __abs__ (Euclidean norm). Verify with test cases.

ex-sp-ch03-03

Easy

Write a base class Estimator with an @abstractmethod called estimate that takes y: np.ndarray and returns np.ndarray. Create two subclasses: MeanEstimator (returns the mean repeated) and MedianEstimator (returns the median repeated). Verify that attempting Estimator() raises TypeError.

ex-sp-ch03-04

Easy

Define a typing.Protocol called HasLength with a single method __len__() -> int. Make it @runtime_checkable. Verify that list, str, np.ndarray, and dict all satisfy the protocol using isinstance.

ex-sp-ch03-05

Easy

Create a class NamedArray that wraps a NumPy array with a name attribute. Implement __array__ so that np.asarray(named_array) returns the underlying data. Verify that np.mean(NamedArray("temperature", data)) works.

ex-sp-ch03-06

Medium

Implement a Solver base class (ABC) with methods solve(A, y) -> np.ndarray and convergence_history() -> list[float]. Create two subclasses: GradientDescentSolver and ProximalSolver. Each should track the cost at every iteration. Write a function compare_solvers(solvers, A, y) that runs all solvers and returns a dict mapping solver names to their final costs.

ex-sp-ch03-07

Medium

Design a ForwardOperator protocol with forward(x), adjoint(y), and a shape property. Implement three concrete operators: DenseMatrix (stores full matrix), DiagonalOperator (stores only diagonal), and FFTOperator (uses FFT/IFFT). Write a function that accepts any ForwardOperator and computes A^T A x for a given x.

ex-sp-ch03-08

Medium

Implement a UnitArray class with __array_ufunc__ that preserves unit strings through addition (same units required), multiplication (units concatenated), and division (units as fraction). Raise ValueError for addition of different units.

ex-sp-ch03-09

Medium

Implement the strategy pattern for a signal processing pipeline. Define a Filter protocol with apply(signal) -> signal. Create three filters: LowPassFilter, HighPassFilter, and BandPassFilter. Build a Pipeline class that chains any sequence of filters.

ex-sp-ch03-10

Medium

Implement dependency injection for a MonteCarloSimulation class. It should accept a DataGenerator, Solver, and MetricsComputer as constructor arguments. Write a test using mock implementations that return fixed values.

ex-sp-ch03-11

Medium

Create a ChannelModel ABC with abstract methods apply(x) -> y and get_matrix(n_rx, n_tx) -> np.ndarray. Implement AWGNChannel, RayleighChannel, and RicianChannel subclasses. Show the MRO for a class that inherits from both RayleighChannel and a LoggingMixin.

ex-sp-ch03-12

Hard

Build a complete SignalArray class that wraps a NumPy array with metadata (sample_rate, channel_name) and implements both __array_ufunc__ and __array_function__. Support np.concatenate (all arrays must have the same sample_rate) and np.mean/np.std (return plain floats). Write at least 5 test cases demonstrating the interop.

ex-sp-ch03-13

Hard

Implement a Plugin system using Protocols. Define a PluginProtocol with name: str, version: str, and execute(data) -> data. Create a PluginManager that discovers plugins, validates they conform to the protocol, and runs them in a configurable order. Include error handling for plugins that fail.

ex-sp-ch03-14

Hard

Design a ComposablePipeline that uses the builder pattern to chain processing steps. Each step conforms to a Step protocol with process(data) -> data. The pipeline should support: adding steps, removing steps by name, reordering steps, and running with intermediate result logging. Compare this design to a deep inheritance hierarchy.

ex-sp-ch03-15

Hard

Implement a MemoizedOperator wrapper that caches the result of a ForwardOperator's forward() method using a hash of the input array. Use composition (not inheritance) to wrap any operator. Implement cache statistics (hits, misses, hit rate) and a clear_cache() method.

ex-sp-ch03-16

Hard

Create a multi-backend Tensor class that dispatches operations to NumPy or CuPy depending on a backend attribute. Implement __array_ufunc__ that routes ufunc calls to the appropriate backend. The class should support .to("numpy") and .to("cupy") for backend transfer (use NumPy-only for the implementation, simulating CuPy with a wrapper).

ex-sp-ch03-17

Challenge

Design and implement a complete simulation framework using all the patterns from this chapter. The framework should:

  1. Use @dataclass(frozen=True) for experiment configuration
  2. Define ForwardOperator and Denoiser protocols
  3. Implement at least 2 operators and 2 denoisers
  4. Use composition to build solvers from (operator, denoiser) pairs
  5. Include dependency injection for testability
  6. Support serialization of configs and results to JSON
  7. Run a parameter sweep over SNR values and plot convergence curves

The entire framework should be under 200 lines of code.

ex-sp-ch03-18

Challenge

Extend the PhysicalQuantity class from this chapter to support:

  1. Full __array_function__ protocol (at least np.concatenate, np.stack, np.linalg.norm, np.fft.fft)
  2. Unit algebra: m * m = m^2, m / s = m*s^-1, automatic simplification
  3. Unit conversion: quantity.to("km") converts meters to kilometers
  4. A registry of known unit conversions loaded from a YAML file
  5. Integration with matplotlib for auto-labeled axes

Write comprehensive tests including edge cases (zero-dimensional arrays, complex units, broadcasting).